You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@syncope.apache.org by sk...@apache.org on 2020/04/30 15:34:06 UTC

[syncope] branch master updated: [SYNCOPE-1555] Allow WA as SAMLs IdP to fetch metadata over REST (#178)

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

skylark17 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 a77e29d  [SYNCOPE-1555] Allow WA as SAMLs IdP to fetch metadata over REST (#178)
a77e29d is described below

commit a77e29d673296b43681f3e7039e55921cb5edda0
Author: Matteo <ma...@users.noreply.github.com>
AuthorDate: Thu Apr 30 17:33:58 2020 +0200

    [SYNCOPE-1555] Allow WA as SAMLs IdP to fetch metadata over REST (#178)
---
 .../syncope/common/lib/to/SAML2IdPMetadataTO.java  | 182 +++++++++++++++++++++
 .../syncope/common/lib/types/AMEntitlement.java    |   6 +
 .../common/rest/api/service/AuthModuleService.java |   6 +-
 .../api/service/SAML2IdPMetadataConfService.java   |  63 +++++++
 .../rest/api/service/SAML2IdPMetadataService.java  |  97 +++++++++++
 .../syncope/core/logic/SAML2IdPMetadataLogic.java  | 120 ++++++++++++++
 .../service/SAML2IdPMetadataConfServiceImpl.java   |  37 +++++
 .../cxf/service/SAML2IdPMetadataServiceImpl.java   |  54 ++++++
 .../api/dao/auth/SAML2IdPMetadataDAO.java          |  32 ++++
 .../api/entity/auth/SAML2IdPMetadata.java          |  49 ++++++
 .../jpa/dao/auth/JPASAML2IdPMetadataDAO.java       |  62 +++++++
 .../persistence/jpa/entity/JPAEntityFactory.java   |   4 +
 .../jpa/entity/auth/JPASAML2IdPMetadata.java       | 119 ++++++++++++++
 .../jpa/inner/SAML2IdPMetadataTest.java            |  84 ++++++++++
 .../api/data/SAML2IdPMetadataBinder.java           |  32 ++++
 .../java/data/SAML2IdPMetadataBinderImpl.java      |  80 +++++++++
 .../org/apache/syncope/fit/AbstractITCase.java     |  21 +++
 .../syncope/fit/core/SAML2IdPMetadataITCase.java   | 113 +++++++++++++
 .../java/org/apache/syncope/wa/WARestClient.java   |  14 +-
 .../bootstrap/SyncopeWABootstrapConfiguration.java |   1 +
 .../bootstrap/SyncopeWAPropertySourceLocator.java  |   2 +-
 .../metadata/RestfulSamlIdPMetadataGenerator.java  |  97 +++++++++++
 .../metadata/RestfulSamlIdPMetadataLocator.java    |  97 +++++++++++
 .../syncope/wa/starter/SyncopeWAApplication.java   |  32 +---
 .../syncope/wa/starter/SyncopeWAConfiguration.java |  49 +++++-
 .../wa/starter/SyncopeWARefreshContextJob.java     |  64 ++++++++
 26 files changed, 1479 insertions(+), 38 deletions(-)

diff --git a/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/SAML2IdPMetadataTO.java b/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/SAML2IdPMetadataTO.java
new file mode 100644
index 0000000..54c0030
--- /dev/null
+++ b/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/SAML2IdPMetadataTO.java
@@ -0,0 +1,182 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.common.lib.to;
+
+import javax.ws.rs.PathParam;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import org.apache.syncope.common.lib.BaseBean;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlType;
+
+@XmlRootElement(name = "saml2idpMetadata")
+@XmlType
+public class SAML2IdPMetadataTO extends BaseBean implements EntityTO {
+
+    private static final long serialVersionUID = 7215073386484048953L;
+
+    private String key;
+
+    private String metadata;
+
+    private String signingCertificate;
+
+    private String signingKey;
+
+    private String encryptionCertificate;
+
+    private String encryptionKey;
+
+    private String appliesTo;
+
+    public static class Builder {
+
+        private final SAML2IdPMetadataTO instance = new SAML2IdPMetadataTO();
+
+        public Builder metadata(final String metadata) {
+            instance.setMetadata(metadata);
+            return this;
+        }
+
+        public Builder signingCertificate(final String signingCertificate) {
+            instance.setSigningCertificate(signingCertificate);
+            return this;
+        }
+
+        public Builder signingKey(final String signingKey) {
+            instance.setSigningKey(signingKey);
+            return this;
+        }
+
+        public Builder encryptionCertificate(final String encryptionCertificate) {
+            instance.setEncryptionCertificate(encryptionCertificate);
+            return this;
+        }
+
+        public Builder encryptionKey(final String encryptionKey) {
+            instance.setEncryptionKey(encryptionKey);
+            return this;
+        }
+
+        public Builder appliesTo(final String appliesTo) {
+            instance.setAppliesTo(appliesTo);
+            return this;
+        }
+
+        public SAML2IdPMetadataTO build() {
+            return instance;
+        }
+    }
+
+    @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 getSigningCertificate() {
+        return signingCertificate;
+    }
+
+    public void setSigningCertificate(final String signingCertificate) {
+        this.signingCertificate = signingCertificate;
+    }
+
+    public String getSigningKey() {
+        return signingKey;
+    }
+
+    public void setSigningKey(final String signingKey) {
+        this.signingKey = signingKey;
+    }
+
+    public String getEncryptionCertificate() {
+        return encryptionCertificate;
+    }
+
+    public void setEncryptionCertificate(final String encryptionCertificate) {
+        this.encryptionCertificate = encryptionCertificate;
+    }
+
+    public String getEncryptionKey() {
+        return encryptionKey;
+    }
+
+    public void setEncryptionKey(final String encryptionKey) {
+        this.encryptionKey = encryptionKey;
+    }
+
+    public String getAppliesTo() {
+        return appliesTo;
+    }
+
+    public void setAppliesTo(final String appliesTo) {
+        this.appliesTo = appliesTo;
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        SAML2IdPMetadataTO other = (SAML2IdPMetadataTO) obj;
+        return new EqualsBuilder().
+                append(key, other.key).
+                append(metadata, other.metadata).
+                append(encryptionCertificate, other.encryptionCertificate).
+                append(encryptionKey, other.encryptionKey).
+                append(signingCertificate, other.signingCertificate).
+                append(signingKey, other.signingKey).
+                append(appliesTo, other.appliesTo).
+                build();
+    }
+
+    @Override
+    public int hashCode() {
+        return new HashCodeBuilder().
+                append(key).
+                append(metadata).
+                append(encryptionCertificate).
+                append(encryptionKey).
+                append(signingCertificate).
+                append(signingKey).
+                append(appliesTo).
+                build();
+    }
+
+}
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 5938dd9..eefe798 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
@@ -54,6 +54,12 @@ public final class AMEntitlement {
 
     public static final String AUTH_MODULE_DELETE = "AUTH_MODULE_DELETE";
 
+    public static final String SAML2_IDP_METADATA_CREATE = "SAML2_IDP_METADATA_CREATE";
+
+    public static final String SAML2_IDP_METADATA_UPDATE = "SAML2_IDP_METADATA_UPDATE";
+
+    public static final String SAML2_IDP_METADATA_READ = "SAML2_IDP_METADATA_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/AuthModuleService.java b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/AuthModuleService.java
index d617b42..57cce16 100644
--- a/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/AuthModuleService.java
+++ b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/AuthModuleService.java
@@ -65,9 +65,9 @@ public interface AuthModuleService extends JAXRSService {
     AuthModuleTO read(@NotNull @PathParam("key") String key);
 
     /**
-     * Returns a list of authentication modules of the matching type.
+     * Returns a list of authentication modules.
      *
-     * @return list of authentication modules with matching type
+     * @return list of authentication modules
      */
     @GET
     @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML })
@@ -76,7 +76,7 @@ public interface AuthModuleService extends JAXRSService {
     /**
      * Create a new authentication module.
      *
-     * @param authModuleTO AuthModule to be created (needs to match type)
+     * @param authModuleTO AuthModule to be created.
      * @return Response object featuring Location header of created authentication module
      */
     @ApiResponses(
diff --git a/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/SAML2IdPMetadataConfService.java b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/SAML2IdPMetadataConfService.java
new file mode 100644
index 0000000..fafe273
--- /dev/null
+++ b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/SAML2IdPMetadataConfService.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.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 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;
+import org.apache.syncope.common.lib.to.SAML2IdPMetadataTO;
+import org.apache.syncope.common.rest.api.RESTHeaders;
+
+/**
+ * REST operations for SAML 2.0 IdP metadata.
+ */
+@Tag(name = "SAML 2.0 IdP Metadata")
+@SecurityRequirements({
+    @SecurityRequirement(name = "BasicAuthentication"),
+    @SecurityRequirement(name = "Bearer") })
+@Path("saml2idp/conf/metadata")
+public interface SAML2IdPMetadataConfService extends JAXRSService {
+
+    /**
+     * Updates SAML 2.0 IdP metadata matching the given key.
+     *
+     * @param saml2IdPMetadataTO SAML2IdPMetadata to replace existing SAML 2.0 IdP metadata
+     */
+    @Parameter(name = "key", description = "SAML2IdPMetadata'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 SAML2IdPMetadataTO saml2IdPMetadataTO);
+
+}
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/SAML2IdPMetadataService.java
new file mode 100644
index 0000000..7791372
--- /dev/null
+++ b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/SAML2IdPMetadataService.java
@@ -0,0 +1,97 @@
+/*
+ * 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.headers.Header;
+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 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;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+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.
+ */
+@Tag(name = "SAML 2.0 IdP Metadata")
+@SecurityRequirements({
+    @SecurityRequirement(name = "BasicAuthentication"),
+    @SecurityRequirement(name = "Bearer") })
+@Path("saml2idp/metadata")
+public interface SAML2IdPMetadataService extends JAXRSService {
+
+    /**
+     * Returns a document outlining keys and metadata of Syncope as SAML 2.0 IdP.
+     *
+     * @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
+     */
+    @GET
+    @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML })
+    SAML2IdPMetadataTO get(@QueryParam("appliesTo") @DefaultValue("Syncope") String appliesTo);
+
+    /**
+     * Returns the SAML 2.0 IdP metadata matching the given key.
+     *
+     * @param key key of requested SAML 2.0 IdP metadata
+     * @return SAML 2.0 IdP metadata with matching id
+     */
+    @GET
+    @Path("{key}")
+    @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML })
+    SAML2IdPMetadataTO read(@NotNull @PathParam("key") String key);
+
+    /**
+     * Store the metadata and keys 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
+     */
+    @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") }),
+        @ApiResponse(responseCode = "409",
+                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);
+
+}
diff --git a/core/am/logic/src/main/java/org/apache/syncope/core/logic/SAML2IdPMetadataLogic.java b/core/am/logic/src/main/java/org/apache/syncope/core/logic/SAML2IdPMetadataLogic.java
new file mode 100644
index 0000000..a6f4930
--- /dev/null
+++ b/core/am/logic/src/main/java/org/apache/syncope/core/logic/SAML2IdPMetadataLogic.java
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.core.logic;
+
+import static org.apache.syncope.core.logic.AbstractLogic.LOG;
+
+import java.lang.reflect.Method;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.to.SAML2IdPMetadataTO;
+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.SAML2IdPMetadataDAO;
+import org.apache.syncope.core.persistence.api.entity.auth.SAML2IdPMetadata;
+import org.apache.syncope.core.provisioning.api.data.SAML2IdPMetadataBinder;
+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;
+
+@Component
+public class SAML2IdPMetadataLogic extends AbstractTransactionalLogic<SAML2IdPMetadataTO> {
+
+    @Autowired
+    private SAML2IdPMetadataBinder binder;
+
+    @Autowired
+    private SAML2IdPMetadataDAO saml2IdPMetadataDAO;
+
+    @PreAuthorize("hasRole('" + AMEntitlement.SAML2_IDP_METADATA_READ + "') "
+            + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+    @Transactional(readOnly = true)
+    public SAML2IdPMetadataTO read(final String key) {
+        SAML2IdPMetadata sAML2IdPMetadata = saml2IdPMetadataDAO.find(key);
+        if (sAML2IdPMetadata == null) {
+            throw new NotFoundException("AuthModule " + key + " not found");
+        }
+
+        return binder.getSAML2IdPMetadataTO(sAML2IdPMetadata);
+    }
+
+    @PreAuthorize("hasRole('" + AMEntitlement.SAML2_IDP_METADATA_READ + "') "
+            + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+    @Transactional(readOnly = true)
+    public SAML2IdPMetadataTO get(final String appliesTo) {
+        SAML2IdPMetadata saml2IdPMetadata = saml2IdPMetadataDAO.findByOwner(appliesTo);
+        if (saml2IdPMetadata == null) {
+            throw new NotFoundException("SAML2 IdP Metadata owned by " + appliesTo + " not found");
+        }
+
+        return binder.getSAML2IdPMetadataTO(saml2IdPMetadata);
+    }
+
+    @PreAuthorize("hasRole('" + AMEntitlement.SAML2_IDP_METADATA_CREATE + "') "
+            + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+    public SAML2IdPMetadataTO set(final SAML2IdPMetadataTO saml2IdPMetadataTO) {
+        SAML2IdPMetadata saml2IdPMetadata = saml2IdPMetadataDAO.findByOwner(saml2IdPMetadataTO.getAppliesTo());
+        if (saml2IdPMetadata == null) {
+            return binder.getSAML2IdPMetadataTO(saml2IdPMetadataDAO.save(binder.create(saml2IdPMetadataTO)));
+        }
+
+        throw SyncopeClientException.build(ClientExceptionType.EntityExists);
+    }
+
+    @PreAuthorize("hasRole('" + AMEntitlement.SAML2_IDP_METADATA_UPDATE + "')")
+    public SAML2IdPMetadataTO update(final SAML2IdPMetadataTO saml2IdPMetadataTO) {
+        SAML2IdPMetadata authModule = saml2IdPMetadataDAO.findByOwner(saml2IdPMetadataTO.getAppliesTo());
+        if (authModule == null) {
+            throw new NotFoundException("AuthModule " + saml2IdPMetadataTO.getKey() + " not found");
+        }
+
+        return binder.getSAML2IdPMetadataTO(saml2IdPMetadataDAO.save(binder.update(authModule, saml2IdPMetadataTO)));
+    }
+
+    @Override
+    protected SAML2IdPMetadataTO resolveReference(final Method method, final Object... args)
+            throws UnresolvedReferenceException {
+
+        String appliesTo = null;
+
+        if (ArrayUtils.isNotEmpty(args)) {
+            for (int i = 0; appliesTo == null && i < args.length; i++) {
+                if (args[i] instanceof String) {
+                    appliesTo = (String) args[i];
+                } else if (args[i] instanceof SAML2IdPMetadataTO) {
+                    appliesTo = ((SAML2IdPMetadataTO) args[i]).getKey();
+                }
+            }
+        }
+
+        if (appliesTo != null) {
+            try {
+                return binder.getSAML2IdPMetadataTO(saml2IdPMetadataDAO.findByOwner(appliesTo));
+            } catch (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/SAML2IdPMetadataConfServiceImpl.java b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/SAML2IdPMetadataConfServiceImpl.java
new file mode 100644
index 0000000..12b6cb6
--- /dev/null
+++ b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/SAML2IdPMetadataConfServiceImpl.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.SAML2IdPMetadataTO;
+import org.apache.syncope.common.rest.api.service.SAML2IdPMetadataConfService;
+import org.apache.syncope.core.logic.SAML2IdPMetadataLogic;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class SAML2IdPMetadataConfServiceImpl extends AbstractServiceImpl implements SAML2IdPMetadataConfService {
+
+    @Autowired
+    private SAML2IdPMetadataLogic logic;
+
+    @Override
+    public void update(final SAML2IdPMetadataTO saml2IdPMetadataTO) {
+        logic.update(saml2IdPMetadataTO);
+    }
+}
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/SAML2IdPMetadataServiceImpl.java
new file mode 100644
index 0000000..b1eb6a3
--- /dev/null
+++ b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/SAML2IdPMetadataServiceImpl.java
@@ -0,0 +1,54 @@
+/*
+ * 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 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.core.logic.SAML2IdPMetadataLogic;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class SAML2IdPMetadataServiceImpl extends AbstractServiceImpl implements SAML2IdPMetadataService {
+
+    @Autowired
+    private SAML2IdPMetadataLogic logic;
+
+    @Override
+    public SAML2IdPMetadataTO get(final String appliesTo) {
+        return logic.get(appliesTo);
+    }
+
+    @Override
+    public SAML2IdPMetadataTO 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();
+        return Response.created(location).
+                header(RESTHeaders.RESOURCE_KEY, saml2IdPMetadata.getKey()).
+                build();
+    }
+}
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/auth/SAML2IdPMetadataDAO.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/auth/SAML2IdPMetadataDAO.java
new file mode 100644
index 0000000..b17688b
--- /dev/null
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/auth/SAML2IdPMetadataDAO.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.SAML2IdPMetadata;
+
+public interface SAML2IdPMetadataDAO extends DAO<SAML2IdPMetadata> {
+
+    SAML2IdPMetadata find(String key);
+
+    SAML2IdPMetadata findByOwner(String appliesTo);
+
+    SAML2IdPMetadata save(SAML2IdPMetadata saml2IdPMetadata);
+
+}
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/auth/SAML2IdPMetadata.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/auth/SAML2IdPMetadata.java
new file mode 100644
index 0000000..a91eb61
--- /dev/null
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/auth/SAML2IdPMetadata.java
@@ -0,0 +1,49 @@
+/*
+ * 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 SAML2IdPMetadata extends Entity {
+
+    String getMetadata();
+
+    void setMetadata(String metadata);
+
+    String getSigningCertificate();
+
+    void setSigningCertificate(String signingCertificate);
+
+    String getSigningKey();
+
+    void setSigningKey(String signingKey);
+
+    String getEncryptionCertificate();
+
+    void setEncryptionCertificate(String encryptionCertificate);
+
+    String getEncryptionKey();
+
+    void setEncryptionKey(String encryptionKey);
+
+    String getAppliesTo();
+
+    void setAppliesTo(String appliesTo);
+
+}
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/auth/JPASAML2IdPMetadataDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/auth/JPASAML2IdPMetadataDAO.java
new file mode 100644
index 0000000..848679e
--- /dev/null
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/auth/JPASAML2IdPMetadataDAO.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 javax.persistence.NoResultException;
+import javax.persistence.TypedQuery;
+import org.apache.syncope.core.persistence.jpa.dao.AbstractDAO;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+import org.apache.syncope.core.persistence.api.dao.auth.SAML2IdPMetadataDAO;
+import org.apache.syncope.core.persistence.api.entity.auth.SAML2IdPMetadata;
+import org.apache.syncope.core.persistence.jpa.entity.auth.JPASAML2IdPMetadata;
+
+@Repository
+public class JPASAML2IdPMetadataDAO extends AbstractDAO<SAML2IdPMetadata> implements SAML2IdPMetadataDAO {
+
+    @Transactional(readOnly = true)
+    @Override
+    public SAML2IdPMetadata find(final String key) {
+        return entityManager().find(JPASAML2IdPMetadata.class, key);
+    }
+
+    @Transactional(readOnly = true)
+    @Override
+    public SAML2IdPMetadata findByOwner(final String appliesTo) {
+        TypedQuery<SAML2IdPMetadata> query = entityManager().createQuery(
+                "SELECT e FROM " + JPASAML2IdPMetadata.class.getSimpleName() + " e WHERE e.appliesTo=:appliesTo",
+                SAML2IdPMetadata.class);
+        query.setParameter("appliesTo", appliesTo);
+
+        SAML2IdPMetadata result = null;
+        try {
+            result = query.getSingleResult();
+        } catch (final NoResultException e) {
+            LOG.debug("No SAML2 IdP Metadata found with appliesTo = {}", appliesTo);
+        }
+
+        return result;
+    }
+
+    @Override
+    public SAML2IdPMetadata save(final SAML2IdPMetadata 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 a034449..28e6e66 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
@@ -59,6 +59,7 @@ import org.apache.syncope.core.persistence.api.entity.anyobject.AnyObject;
 import org.apache.syncope.core.persistence.api.entity.auth.AuthModule;
 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.group.GPlainAttr;
 import org.apache.syncope.core.persistence.api.entity.group.GPlainAttrUniqueValue;
@@ -155,6 +156,7 @@ import org.apache.syncope.core.persistence.jpa.entity.user.JPAUser;
 import org.apache.syncope.core.spring.security.SecureRandomUtils;
 import org.apache.syncope.core.persistence.jpa.entity.auth.JPAAuthModule;
 import org.apache.syncope.core.persistence.jpa.entity.auth.JPAAuthModuleItem;
+import org.apache.syncope.core.persistence.jpa.entity.auth.JPASAML2IdPMetadata;
 
 public class JPAEntityFactory implements EntityFactory {
 
@@ -321,6 +323,8 @@ public class JPAEntityFactory implements EntityFactory {
             result = (E) new JPAOIDCRP();
         } else if (reference.equals(SAML2SP.class)) {
             result = (E) new JPASAML2SP();
+        } else if (reference.equals(SAML2IdPMetadata.class)) {
+            result = (E) new JPASAML2IdPMetadata();
         } 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/JPASAML2IdPMetadata.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/auth/JPASAML2IdPMetadata.java
new file mode 100644
index 0000000..1bb0130
--- /dev/null
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/auth/JPASAML2IdPMetadata.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.core.persistence.jpa.entity.auth;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Lob;
+import javax.persistence.Table;
+import org.apache.syncope.core.persistence.api.entity.auth.SAML2IdPMetadata;
+import org.apache.syncope.core.persistence.jpa.entity.AbstractGeneratedKeyEntity;
+
+@Entity
+@Table(name = JPASAML2IdPMetadata.TABLE)
+public class JPASAML2IdPMetadata extends AbstractGeneratedKeyEntity implements SAML2IdPMetadata {
+
+    public static final String TABLE = "SAML2IdPMetadata";
+
+    private static final long serialVersionUID = 57352617217394093L;
+
+    @Column(unique = true)
+    private String appliesTo;
+
+    @Lob
+    @Column
+    private String metadata;
+
+    @Lob
+    @Column
+    private String signingCertificate;
+
+    @Lob
+    @Column
+    private String signingKey;
+
+    @Lob
+    @Column
+    private String encryptionCertificate;
+
+    @Lob
+    @Column
+    private String encryptionKey;
+
+    @Override
+    public String getMetadata() {
+        return metadata;
+    }
+
+    @Override
+    public void setMetadata(final String metadata) {
+        this.metadata = metadata;
+    }
+
+    @Override
+    public String getSigningCertificate() {
+        return signingCertificate;
+    }
+
+    @Override
+    public void setSigningCertificate(final String signingCertificate) {
+        this.signingCertificate = signingCertificate;
+    }
+
+    @Override
+    public String getSigningKey() {
+        return signingKey;
+    }
+
+    @Override
+    public void setSigningKey(final String signingKey) {
+        this.signingKey = signingKey;
+    }
+
+    @Override
+    public String getEncryptionCertificate() {
+        return encryptionCertificate;
+    }
+
+    @Override
+    public void setEncryptionCertificate(final String encryptionCertificate) {
+        this.encryptionCertificate = encryptionCertificate;
+    }
+
+    @Override
+    public String getEncryptionKey() {
+        return encryptionKey;
+    }
+
+    @Override
+    public void setEncryptionKey(final String encryptionKey) {
+        this.encryptionKey = encryptionKey;
+    }
+
+    @Override
+    public String getAppliesTo() {
+        return appliesTo;
+    }
+
+    @Override
+    public void setAppliesTo(final String appliesTo) {
+        this.appliesTo = appliesTo;
+    }
+
+}
diff --git a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/SAML2IdPMetadataTest.java b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/SAML2IdPMetadataTest.java
new file mode 100644
index 0000000..8d47b48
--- /dev/null
+++ b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/SAML2IdPMetadataTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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 static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.util.UUID;
+import org.apache.syncope.core.persistence.api.dao.auth.SAML2IdPMetadataDAO;
+import org.apache.syncope.core.persistence.jpa.AbstractTest;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
+import org.apache.syncope.core.persistence.api.entity.auth.SAML2IdPMetadata;
+
+@Transactional("Master")
+public class SAML2IdPMetadataTest extends AbstractTest {
+
+    @Autowired
+    private SAML2IdPMetadataDAO saml2IdPMetadataDAO;
+
+    @Test
+    public void find() {
+        create("Syncope");
+        SAML2IdPMetadata saml2IdPMetadata = saml2IdPMetadataDAO.findByOwner("Syncope");
+        assertNotNull(saml2IdPMetadata);
+
+        saml2IdPMetadata = saml2IdPMetadataDAO.findByOwner(UUID.randomUUID().toString());
+        assertNull(saml2IdPMetadata);
+    }
+
+    @Test
+    public void save() {
+        create("SyncopeCreate");
+    }
+
+    @Test
+    public void update() {
+        SAML2IdPMetadata saml2IdPMetadata = create("SyncopeUpdate");
+        assertNotNull(saml2IdPMetadata);
+        saml2IdPMetadata.setAppliesTo("OtherSyncope");
+
+        saml2IdPMetadata = saml2IdPMetadataDAO.save(saml2IdPMetadata);
+        assertNotNull(saml2IdPMetadata);
+        assertNotNull(saml2IdPMetadata.getKey());
+        SAML2IdPMetadata found = saml2IdPMetadataDAO.findByOwner(saml2IdPMetadata.getAppliesTo());
+        assertNotNull(found);
+        assertEquals("OtherSyncope", found.getAppliesTo());
+    }
+
+    private SAML2IdPMetadata create(final String appliesTo) {
+        SAML2IdPMetadata saml2IdPMetadata = entityFactory.newEntity(SAML2IdPMetadata.class);
+        saml2IdPMetadata.setAppliesTo(appliesTo);
+        saml2IdPMetadata.setMetadata("metadata");
+        saml2IdPMetadata.setEncryptionCertificate("encryptionCert");
+        saml2IdPMetadata.setEncryptionKey("encryptionKey");
+        saml2IdPMetadata.setSigningCertificate("signatureCert");
+        saml2IdPMetadata.setSigningKey("signatureKey");
+        saml2IdPMetadataDAO.save(saml2IdPMetadata);
+        assertNotNull(saml2IdPMetadata);
+        assertNotNull(saml2IdPMetadata.getKey());
+        assertNotNull(saml2IdPMetadataDAO.findByOwner(saml2IdPMetadata.getAppliesTo()));
+
+        return saml2IdPMetadata;
+    }
+
+}
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/SAML2IdPMetadataBinder.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/SAML2IdPMetadataBinder.java
new file mode 100644
index 0000000..e2e190e
--- /dev/null
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/SAML2IdPMetadataBinder.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.SAML2IdPMetadataTO;
+import org.apache.syncope.core.persistence.api.entity.auth.SAML2IdPMetadata;
+
+public interface SAML2IdPMetadataBinder {
+
+    SAML2IdPMetadata create(SAML2IdPMetadataTO saml2IdPMetadataTO);
+
+    SAML2IdPMetadata update(SAML2IdPMetadata saml2IdPMetadata, SAML2IdPMetadataTO saml2IdPMetadataTO);
+
+    SAML2IdPMetadataTO getSAML2IdPMetadataTO(SAML2IdPMetadata saml2IdPMetadata);
+
+}
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/SAML2IdPMetadataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/SAML2IdPMetadataBinderImpl.java
new file mode 100644
index 0000000..1f4d6fa
--- /dev/null
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/SAML2IdPMetadataBinderImpl.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.core.provisioning.java.data;
+
+import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.apache.syncope.common.lib.to.SAML2IdPMetadataTO;
+import org.apache.syncope.core.persistence.api.entity.auth.SAML2IdPMetadata;
+import org.apache.syncope.core.provisioning.api.data.SAML2IdPMetadataBinder;
+
+@Component
+public class SAML2IdPMetadataBinderImpl implements SAML2IdPMetadataBinder {
+
+    @Autowired
+    private EntityFactory entityFactory;
+
+    private SAML2IdPMetadata getSAML2IdPMetadata(
+            final SAML2IdPMetadata saml2IdPMetadata,
+            final SAML2IdPMetadataTO saml2IdPMetadataTO) {
+
+        SAML2IdPMetadata saml2IdPMetadataResult = saml2IdPMetadata;
+        if (saml2IdPMetadataResult == null) {
+            saml2IdPMetadataResult = entityFactory.newEntity(SAML2IdPMetadata.class);
+        }
+
+        saml2IdPMetadataResult.setEncryptionCertificate(saml2IdPMetadataTO.getEncryptionCertificate());
+        saml2IdPMetadataResult.setEncryptionKey(saml2IdPMetadataTO.getEncryptionKey());
+        saml2IdPMetadataResult.setMetadata(saml2IdPMetadataTO.getMetadata());
+        saml2IdPMetadataResult.setSigningCertificate(saml2IdPMetadataTO.getSigningCertificate());
+        saml2IdPMetadataResult.setSigningKey(saml2IdPMetadataTO.getSigningKey());
+        saml2IdPMetadataResult.setAppliesTo(saml2IdPMetadataTO.getAppliesTo());
+
+        return saml2IdPMetadataResult;
+    }
+
+    @Override
+    public SAML2IdPMetadata create(final SAML2IdPMetadataTO saml2IdPMetadataTO) {
+        return update(entityFactory.newEntity(SAML2IdPMetadata.class), saml2IdPMetadataTO);
+    }
+
+    @Override
+    public SAML2IdPMetadata update(
+            final SAML2IdPMetadata saml2IdPMetadata,
+            final SAML2IdPMetadataTO saml2IdPMetadataTO) {
+
+        return getSAML2IdPMetadata(saml2IdPMetadata, saml2IdPMetadataTO);
+    }
+
+    @Override
+    public SAML2IdPMetadataTO getSAML2IdPMetadataTO(final SAML2IdPMetadata saml2IdPMetadata) {
+        SAML2IdPMetadataTO saml2IdPMetadataTO = new SAML2IdPMetadataTO();
+
+        saml2IdPMetadataTO.setKey(saml2IdPMetadata.getKey());
+        saml2IdPMetadataTO.setMetadata(saml2IdPMetadata.getMetadata());
+        saml2IdPMetadataTO.setEncryptionCertificate(saml2IdPMetadata.getEncryptionCertificate());
+        saml2IdPMetadataTO.setEncryptionKey(saml2IdPMetadata.getEncryptionKey());
+        saml2IdPMetadataTO.setSigningCertificate(saml2IdPMetadata.getSigningCertificate());
+        saml2IdPMetadataTO.setSigningKey(saml2IdPMetadata.getSigningKey());
+        saml2IdPMetadataTO.setAppliesTo(saml2IdPMetadata.getAppliesTo());
+
+        return saml2IdPMetadataTO;
+    }
+}
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 ee19a6b..530113f 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
@@ -77,6 +77,7 @@ import org.apache.syncope.common.lib.to.RoleTO;
 import org.apache.syncope.common.lib.to.UserTO;
 import org.apache.syncope.common.lib.to.AuthModuleTO;
 import org.apache.syncope.common.lib.to.AuthPolicyTO;
+import org.apache.syncope.common.lib.to.SAML2IdPMetadataTO;
 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;
@@ -128,6 +129,8 @@ import org.apache.syncope.common.rest.api.service.UserService;
 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.UserWorkflowTaskService;
 import org.apache.syncope.fit.core.CoreITContext;
 import org.apache.syncope.fit.core.UserITCase;
@@ -280,6 +283,10 @@ public abstract class AbstractITCase {
 
     protected static AuthModuleService authModuleService;
 
+    protected static SAML2IdPMetadataService saml2IdPMetadataService;
+
+    protected static SAML2IdPMetadataConfService saml2IdPMetadataConfService;
+
     protected static SecurityQuestionService securityQuestionService;
 
     protected static ImplementationService implementationService;
@@ -373,6 +380,8 @@ public abstract class AbstractITCase {
         scimConfService = adminClient.getService(SCIMConfService.class);
         clientAppService = adminClient.getService(ClientAppService.class);
         authModuleService = adminClient.getService(AuthModuleService.class);
+        saml2IdPMetadataService = adminClient.getService(SAML2IdPMetadataService.class);
+        saml2IdPMetadataConfService = adminClient.getService(SAML2IdPMetadataConfService.class);
     }
 
     @Autowired
@@ -587,6 +596,18 @@ public abstract class AbstractITCase {
         return getObject(response.getLocation(), AuthModuleService.class, authModule.getClass());
     }
 
+    @SuppressWarnings("unchecked")
+    protected SAML2IdPMetadataTO createSAML2IdPMetadata(final SAML2IdPMetadataTO saml2IdPMetadata) {
+        Response response = saml2IdPMetadataService.set(saml2IdPMetadata);
+        if (response.getStatusInfo().getStatusCode() != Response.Status.CREATED.getStatusCode()) {
+            Exception ex = clientFactory.getExceptionMapper().fromResponse(response);
+            if (ex != null) {
+                throw (RuntimeException) ex;
+            }
+        }
+        return getObject(response.getLocation(), SAML2IdPMetadataService.class, saml2IdPMetadata.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/SAML2IdPMetadataITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SAML2IdPMetadataITCase.java
new file mode 100644
index 0000000..58facb8
--- /dev/null
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SAML2IdPMetadataITCase.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.fit.core;
+
+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;
+
+import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.fit.AbstractITCase;
+import org.junit.jupiter.api.Test;
+import org.apache.syncope.common.lib.to.SAML2IdPMetadataTO;
+import org.apache.syncope.common.lib.types.ClientExceptionType;
+import org.apache.syncope.core.persistence.api.dao.NotFoundException;
+import org.junit.platform.commons.util.StringUtils;
+
+public class SAML2IdPMetadataITCase extends AbstractITCase {
+    
+    private static final String APPLIES_TO = "Syncope";
+
+    private SAML2IdPMetadataTO createSAML2IdPMetadata() {
+        SAML2IdPMetadataTO result = createSAML2IdPMetadata(new SAML2IdPMetadataTO.Builder().
+                appliesTo(APPLIES_TO).
+                metadata("testMetadata").
+                encryptionCertificate("testEncryptionCert").
+                encryptionKey("testEncryptionKey").
+                signingCertificate("testSigningCert").
+                signingKey("testSigningKey").
+                build());
+        assertNotNull(result);
+        testIsValid(result);
+
+        return result;
+    }
+
+    private void testIsValid(final SAML2IdPMetadataTO saml2IdPMetadataTO) {
+        assertFalse(StringUtils.isBlank(saml2IdPMetadataTO.getAppliesTo()));
+        assertFalse(StringUtils.isBlank(saml2IdPMetadataTO.getMetadata()));
+        assertFalse(StringUtils.isBlank(saml2IdPMetadataTO.getEncryptionKey()));
+        assertFalse(StringUtils.isBlank(saml2IdPMetadataTO.getEncryptionCertificate()));
+        assertFalse(StringUtils.isBlank(saml2IdPMetadataTO.getSigningCertificate()));
+        assertFalse(StringUtils.isBlank(saml2IdPMetadataTO.getSigningKey()));
+    }
+
+    @Test
+    public void read() {
+        SAML2IdPMetadataTO saml2IdPMetadataTO = null;
+        try {
+            saml2IdPMetadataTO = saml2IdPMetadataService.get(APPLIES_TO);
+        } catch (SyncopeClientException e) {
+            saml2IdPMetadataTO = createSAML2IdPMetadata();
+        }
+
+        assertNotNull(saml2IdPMetadataTO);
+        assertEquals(APPLIES_TO, saml2IdPMetadataTO.getAppliesTo());
+        testIsValid(saml2IdPMetadataTO);
+    }
+
+    @Test
+    public void create() {
+        try {
+            saml2IdPMetadataService.get(APPLIES_TO);
+        } catch (SyncopeClientException e) {
+            createSAML2IdPMetadata();
+        }
+
+        try {
+            createSAML2IdPMetadata(new SAML2IdPMetadataTO.Builder().
+                    appliesTo(APPLIES_TO).
+                    metadata("testMetadata").
+                    build());
+            fail("This should not happen");
+        } catch (SyncopeClientException e) {
+            assertEquals(ClientExceptionType.EntityExists, e.getType());
+        }
+    }
+
+    @Test
+    public void update() {
+        SAML2IdPMetadataTO saml2IdPMetadataTO = null;
+        try {
+            saml2IdPMetadataTO = saml2IdPMetadataService.get(APPLIES_TO);
+        } catch (NotFoundException e) {
+            saml2IdPMetadataTO = createSAML2IdPMetadata();
+        }
+
+        assertNotNull(saml2IdPMetadataTO);
+        saml2IdPMetadataTO.setEncryptionKey("newKey");
+        saml2IdPMetadataConfService.update(saml2IdPMetadataTO);
+        saml2IdPMetadataTO = saml2IdPMetadataService.get(saml2IdPMetadataTO.getAppliesTo());
+        assertNotNull(saml2IdPMetadataTO);
+
+        assertEquals("newKey", saml2IdPMetadataTO.getEncryptionKey());
+    }
+
+}
diff --git a/wa/bootstrap/src/main/java/org/apache/syncope/wa/WARestClient.java b/wa/bootstrap/src/main/java/org/apache/syncope/wa/WARestClient.java
index ca2d048..aa323d4 100644
--- a/wa/bootstrap/src/main/java/org/apache/syncope/wa/WARestClient.java
+++ b/wa/bootstrap/src/main/java/org/apache/syncope/wa/WARestClient.java
@@ -19,7 +19,6 @@
 package org.apache.syncope.wa;
 
 import org.apereo.cas.util.spring.ApplicationContextProvider;
-
 import org.apache.syncope.client.lib.AnonymousAuthenticationHandler;
 import org.apache.syncope.client.lib.SyncopeClient;
 import org.apache.syncope.client.lib.SyncopeClientFactoryBean;
@@ -29,7 +28,6 @@ import org.apache.syncope.common.keymaster.client.api.model.NetworkService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.context.ApplicationContext;
-
 import java.util.Collection;
 
 public class WARestClient {
@@ -45,9 +43,9 @@ public class WARestClient {
     private SyncopeClient client;
 
     public WARestClient(
-        final String anonymousUser,
-        final String anonymousKey,
-        final boolean useGZIPCompression) {
+            final String anonymousUser,
+            final String anonymousKey,
+            final boolean useGZIPCompression) {
 
         this.anonymousUser = anonymousUser;
         this.anonymousKey = anonymousKey;
@@ -59,9 +57,9 @@ public class WARestClient {
             if (client == null && isReady()) {
                 try {
                     client = new SyncopeClientFactoryBean().
-                        setAddress(getCore().getAddress()).
-                        setUseCompression(useGZIPCompression).
-                        create(new AnonymousAuthenticationHandler(anonymousUser, anonymousKey));
+                            setAddress(getCore().getAddress()).
+                            setUseCompression(useGZIPCompression).
+                            create(new AnonymousAuthenticationHandler(anonymousUser, anonymousKey));
                 } catch (Exception e) {
                     LOG.error("Could not init SyncopeClient", e);
                 }
diff --git a/wa/bootstrap/src/main/java/org/apache/syncope/wa/bootstrap/SyncopeWABootstrapConfiguration.java b/wa/bootstrap/src/main/java/org/apache/syncope/wa/bootstrap/SyncopeWABootstrapConfiguration.java
index 2330c3f..15498fe 100644
--- a/wa/bootstrap/src/main/java/org/apache/syncope/wa/bootstrap/SyncopeWABootstrapConfiguration.java
+++ b/wa/bootstrap/src/main/java/org/apache/syncope/wa/bootstrap/SyncopeWABootstrapConfiguration.java
@@ -30,6 +30,7 @@ import org.springframework.context.annotation.PropertySource;
 @PropertySource("classpath:wa.properties")
 @PropertySource(value = "file:${conf.directory}/wa.properties", ignoreResourceNotFound = true)
 public class SyncopeWABootstrapConfiguration {
+
     @Value("${anonymousUser}")
     private String anonymousUser;
 
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 875ac90..94e50c8 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
@@ -16,7 +16,6 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-
 package org.apache.syncope.wa.bootstrap;
 
 import org.apereo.cas.configuration.CasConfigurationProperties;
@@ -62,6 +61,7 @@ import java.util.stream.Collectors;
 
 @Order
 public class SyncopeWAPropertySourceLocator implements PropertySourceLocator {
+
     private static final Logger LOG = LoggerFactory.getLogger(SyncopeWABootstrapConfiguration.class);
 
     private final WARestClient waRestClient;
diff --git a/wa/starter/src/main/java/org/apache/syncope/wa/saml/idp/metadata/RestfulSamlIdPMetadataGenerator.java b/wa/starter/src/main/java/org/apache/syncope/wa/saml/idp/metadata/RestfulSamlIdPMetadataGenerator.java
new file mode 100644
index 0000000..74bcb04
--- /dev/null
+++ b/wa/starter/src/main/java/org/apache/syncope/wa/saml/idp/metadata/RestfulSamlIdPMetadataGenerator.java
@@ -0,0 +1,97 @@
+/*
+ * 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.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.wa.WARestClient;
+import org.apereo.cas.support.saml.idp.metadata.generator.BaseSamlIdPMetadataGenerator;
+import org.apereo.cas.support.saml.idp.metadata.generator.SamlIdPMetadataGeneratorConfigurationContext;
+import org.apereo.cas.support.saml.services.SamlRegisteredService;
+import org.apereo.cas.support.saml.services.idp.metadata.SamlIdPMetadataDocument;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpStatus;
+import javax.ws.rs.core.Response;
+import java.util.Optional;
+
+public class RestfulSamlIdPMetadataGenerator extends BaseSamlIdPMetadataGenerator {
+
+    private static final Logger LOG = LoggerFactory.getLogger(RestfulSamlIdPMetadataGenerator.class);
+
+    public static final String DEFAULT_APPLIES_FOR = "Syncope";
+
+    private final WARestClient restClient;
+
+    public RestfulSamlIdPMetadataGenerator(
+            final SamlIdPMetadataGeneratorConfigurationContext samlIdPMetadataGeneratorConfigurationContext,
+            final WARestClient restClient) {
+
+        super(samlIdPMetadataGeneratorConfigurationContext);
+        this.restClient = restClient;
+    }
+
+    @Override
+    protected SamlIdPMetadataDocument finalizeMetadataDocument(
+            final SamlIdPMetadataDocument doc,
+            final Optional<SamlRegisteredService> registeredService) {
+
+        LOG.info("Generating new SAML2 IdP metadata document");
+        doc.setAppliesTo(DEFAULT_APPLIES_FOR);
+        SAML2IdPMetadataTO metadataTO = new SAML2IdPMetadataTO.Builder().
+                metadata(doc.getMetadata()).
+                encryptionKey(doc.getEncryptionKey()).
+                encryptionCertificate(doc.getEncryptionCertificate()).
+                signingCertificate(doc.getSigningCertificate()).
+                signingKey(doc.getSigningKey()).
+                appliesTo(doc.getAppliesTo()).
+                build();
+
+        SyncopeClient client = getSyncopeClient();
+        Response response = null;
+        try {
+            response = client.getService(SAML2IdPMetadataService.class).set(metadataTO);
+        } catch (Exception ex) {
+            LOG.warn("While generating SAML2 IdP metadata document", ex);
+        }
+
+        return response != null && HttpStatus.valueOf(response.getStatus()).is2xxSuccessful() ? doc : null;
+    }
+
+    @Override
+    public Pair<String, String> buildSelfSignedEncryptionCert(final Optional<SamlRegisteredService> registeredService) {
+        return generateCertificateAndKey();
+    }
+
+    @Override
+    public Pair<String, String> buildSelfSignedSigningCert(final Optional<SamlRegisteredService> registeredService) {
+        return generateCertificateAndKey();
+    }
+
+    private SyncopeClient getSyncopeClient() {
+        if (!WARestClient.isReady()) {
+            LOG.info("Syncope client is not yet ready");
+            throw new RuntimeException("Syncope core is not yet ready to access requests");
+        }
+        return restClient.getSyncopeClient();
+    }
+
+}
diff --git a/wa/starter/src/main/java/org/apache/syncope/wa/saml/idp/metadata/RestfulSamlIdPMetadataLocator.java b/wa/starter/src/main/java/org/apache/syncope/wa/saml/idp/metadata/RestfulSamlIdPMetadataLocator.java
new file mode 100644
index 0000000..57592e5
--- /dev/null
+++ b/wa/starter/src/main/java/org/apache/syncope/wa/saml/idp/metadata/RestfulSamlIdPMetadataLocator.java
@@ -0,0 +1,97 @@
+/*
+ * 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.saml.idp.metadata;
+
+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.wa.WARestClient;
+import org.apereo.cas.support.saml.idp.metadata.locator.AbstractSamlIdPMetadataLocator;
+import org.apereo.cas.support.saml.services.SamlRegisteredService;
+import org.apereo.cas.support.saml.services.idp.metadata.SamlIdPMetadataDocument;
+import org.apereo.cas.util.crypto.CipherExecutor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.util.Optional;
+
+public class RestfulSamlIdPMetadataLocator extends AbstractSamlIdPMetadataLocator {
+
+    private static final Logger LOG = LoggerFactory.getLogger(RestfulSamlIdPMetadataLocator.class);
+
+    private final WARestClient restClient;
+
+    public RestfulSamlIdPMetadataLocator(
+            final CipherExecutor<String, String> metadataCipherExecutor,
+            final WARestClient restClient) {
+
+        super(metadataCipherExecutor);
+        this.restClient = restClient;
+    }
+
+    private static String getAppliesToFor(final Optional<SamlRegisteredService> result) {
+        if (result.isPresent()) {
+            SamlRegisteredService registeredService = result.get();
+            return registeredService.getName() + '-' + registeredService.getId();
+        }
+        return RestfulSamlIdPMetadataGenerator.DEFAULT_APPLIES_FOR;
+    }
+
+    @Override
+    public SamlIdPMetadataDocument fetchInternal(final Optional<SamlRegisteredService> registeredService) {
+        try {
+            LOG.info("Locating SAML2 IdP metadata document");
+            SAML2IdPMetadataTO saml2IdPMetadataTO = getSyncopeClient().getService(SAML2IdPMetadataService.class).
+                    get(getAppliesToFor(registeredService));
+
+            if (saml2IdPMetadataTO == null) {
+                LOG.warn("No SAML2 IdP metadata document obtained from core");
+            } else {
+                SamlIdPMetadataDocument document = new SamlIdPMetadataDocument();
+                document.setMetadata(saml2IdPMetadataTO.getMetadata());
+                document.setEncryptionCertificate(saml2IdPMetadataTO.getEncryptionCertificate());
+                document.setEncryptionKey(saml2IdPMetadataTO.getEncryptionKey());
+                document.setSigningKey(saml2IdPMetadataTO.getSigningKey());
+                document.setSigningCertificate(saml2IdPMetadataTO.getSigningCertificate());
+                document.setAppliesTo(saml2IdPMetadataTO.getAppliesTo());
+                if (document.isValid()) {
+                    LOG.debug("Found SAML2 IdP metadata document: {}", document.getId());
+                    return document;
+                }
+                LOG.warn("Not a valid SAML2 IdP metadata document");
+            }
+
+            return null;
+        } catch (SyncopeClientException ex) {
+            if (ex.getType() == ClientExceptionType.NotFound) {
+                LOG.debug("No SAML2 IdP metadata document is available");
+            }
+        }
+        return null;
+    }
+
+    private SyncopeClient getSyncopeClient() {
+        if (!WARestClient.isReady()) {
+            LOG.info("Syncope client is not yet ready");
+            throw new RuntimeException("Syncope core is not yet ready to access requests");
+        }
+        return restClient.getSyncopeClient();
+    }
+}
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 1b43386..058274a 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,18 +18,13 @@
  */
 package org.apache.syncope.wa.starter;
 
-import java.time.LocalDateTime;
-import java.time.ZoneId;
-import java.util.Date;
 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.Job;
 import org.quartz.JobBuilder;
 import org.quartz.JobDetail;
-import org.quartz.JobExecutionContext;
 import org.quartz.JobKey;
 import org.quartz.SchedulerException;
 import org.quartz.Trigger;
@@ -55,7 +50,6 @@ import org.springframework.boot.builder.SpringApplicationBuilder;
 import org.springframework.boot.context.event.ApplicationReadyEvent;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
-import org.springframework.cloud.context.refresh.ContextRefresher;
 import org.springframework.context.annotation.EnableAspectJAutoProxy;
 import org.springframework.context.annotation.PropertySource;
 import org.springframework.context.event.EventListener;
@@ -63,6 +57,9 @@ 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;
 
 @PropertySource("classpath:wa.properties")
 @PropertySource(value = "file:${conf.directory}/wa.properties", ignoreResourceNotFound = true)
@@ -90,9 +87,6 @@ public class SyncopeWAApplication extends SpringBootServletInitializer {
     private static final Logger LOG = LoggerFactory.getLogger(SyncopeWAApplication.class);
 
     @Autowired
-    private ContextRefresher contextRefresher;
-
-    @Autowired
     private SchedulerFactoryBean scheduler;
 
     @Value("${contextRefreshDelay:15}")
@@ -134,22 +128,14 @@ public class SyncopeWAApplication extends SpringBootServletInitializer {
             Trigger trigger = TriggerBuilder.newTrigger().startAt(date).build();
             JobKey jobKey = new JobKey(getClass().getSimpleName());
 
-            JobDetail job = JobBuilder.newJob(RefreshApplicationContextJob.class).withIdentity(jobKey).build();
-            scheduler.getScheduler().scheduleJob(job, trigger);
-        } catch (SchedulerException e) {
-            throw new RuntimeException("Could not schedule refresh job", e);
-        }
-    }
+            JobDetail job = JobBuilder.newJob(SyncopeWARefreshContextJob.class).
+                    withIdentity(jobKey).
+                    build();
 
-    private class RefreshApplicationContextJob implements Job {
+            scheduler.getScheduler().scheduleJob(job, trigger);
 
-        @Override
-        public void execute(final JobExecutionContext jobExecutionContext) {
-            try {
-                LOG.debug("Refreshed context: {}", contextRefresher.refresh());
-            } catch (final Exception e) {
-                LOG.error(e.getMessage(), e);
-            }
+        } 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 8b61f58..9ff7c55 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
@@ -21,22 +21,44 @@ package org.apache.syncope.wa.starter;
 import org.apereo.cas.audit.AuditTrailExecutionPlanConfigurer;
 import org.apereo.cas.services.ServiceRegistryExecutionPlanConfigurer;
 import org.apereo.cas.services.ServiceRegistryListener;
-
 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;
 import org.apache.syncope.wa.WARestClient;
+import org.apereo.cas.support.saml.idp.metadata.generator.SamlIdPMetadataGenerator;
+import org.apereo.cas.support.saml.idp.metadata.locator.SamlIdPMetadataLocator;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.context.ConfigurableApplicationContext;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
-
 import java.util.Collection;
+import org.apache.syncope.wa.saml.idp.metadata.RestfulSamlIdPMetadataGenerator;
+import org.apache.syncope.wa.saml.idp.metadata.RestfulSamlIdPMetadataLocator;
+import org.apereo.cas.configuration.CasConfigurationProperties;
+import org.apereo.cas.support.saml.idp.metadata.generator.SamlIdPMetadataGeneratorConfigurationContext;
+import org.apereo.cas.support.saml.idp.metadata.writer.SamlIdPCertificateAndKeyWriter;
+import org.apereo.cas.util.crypto.CipherExecutor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.core.io.ResourceLoader;
 
 @Configuration
 public class SyncopeWAConfiguration {
 
+    private static final Logger LOG = LoggerFactory.getLogger(SyncopeWAConfiguration.class);
+
+    @Autowired
+    private CasConfigurationProperties casProperties;
+
+    @Autowired
+    private ResourceLoader resourceLoader;
+
+    @Autowired
+    @Qualifier("samlSelfSignedCertificateWriter")
+    private ObjectProvider<SamlIdPCertificateAndKeyWriter> samlSelfSignedCertificateWriter;
+
     @Autowired
     private ConfigurableApplicationContext applicationContext;
 
@@ -48,10 +70,30 @@ public class SyncopeWAConfiguration {
     @Bean
     public ServiceRegistryExecutionPlanConfigurer syncopeServiceRegistryConfigurer(final WARestClient restClient) {
         SyncopeServiceRegistry registry =
-            new SyncopeServiceRegistry(restClient, applicationContext, serviceRegistryListeners);
+                new SyncopeServiceRegistry(restClient, applicationContext, serviceRegistryListeners);
         return plan -> plan.registerServiceRegistry(registry);
     }
 
+    @Autowired
+    @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();
+        return new RestfulSamlIdPMetadataGenerator(context, restClient);
+    }
+
+    @Autowired
+    @Bean
+    public SamlIdPMetadataLocator samlIdPMetadataLocator(final WARestClient restClient) {
+        return new RestfulSamlIdPMetadataLocator(CipherExecutor.noOpOfStringToString(), restClient);
+    }
+
     @Bean
     @Autowired
     public AuditTrailExecutionPlanConfigurer auditConfigurer(final WARestClient restClient) {
@@ -67,4 +109,5 @@ public class SyncopeWAConfiguration {
     public KeymasterStop keymasterStop() {
         return new KeymasterStop(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
new file mode 100644
index 0000000..8b3bae3
--- /dev/null
+++ b/wa/starter/src/main/java/org/apache/syncope/wa/starter/SyncopeWARefreshContextJob.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.wa.starter;
+
+import org.apache.syncope.wa.WARestClient;
+import org.apereo.cas.support.saml.idp.metadata.generator.SamlIdPMetadataGenerator;
+import org.apereo.cas.support.saml.services.idp.metadata.SamlIdPMetadataDocument;
+import org.quartz.Job;
+import org.quartz.JobExecutionContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cloud.context.refresh.ContextRefresher;
+import java.util.Optional;
+import org.quartz.JobExecutionException;
+
+public class SyncopeWARefreshContextJob implements Job {
+
+    private static final Logger LOG = LoggerFactory.getLogger(SyncopeWARefreshContextJob.class);
+
+    @Autowired
+    private ContextRefresher contextRefresher;
+
+    @Autowired
+    private SamlIdPMetadataGenerator metadataGenerator;
+
+    public SyncopeWARefreshContextJob() {
+    }
+
+    @Override
+    public void execute(final JobExecutionContext jobExecutionContext) throws JobExecutionException {
+        try {
+            LOG.debug("Refreshing 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("Generating SAML2 IdP metadata metadata");
+            SamlIdPMetadataDocument document = metadataGenerator.generate(Optional.empty());
+            LOG.info("Generated SAML2 IdP metadata for {}", document.getAppliesTo());
+
+        } catch (RuntimeException e) {
+            throw new JobExecutionException("While generating SAML2 IdP metadata", e);
+        }
+    }
+}