You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@syncope.apache.org by il...@apache.org on 2020/04/09 14:24:43 UTC

[syncope] 03/07: [SYNCOPE-1545] ClientApp service

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

ilgrosso pushed a commit to branch SYNCOPE-160
in repository https://gitbox.apache.org/repos/asf/syncope.git

commit e2982772ab45ca4b5c4077e2678f2d16e0251eb2
Author: Francesco Chicchiriccò <il...@apache.org>
AuthorDate: Thu Apr 9 13:35:40 2020 +0200

    [SYNCOPE-1545] ClientApp service
---
 .../syncope/common/lib/to/client/ClientAppTO.java  | 154 ++++++++++++
 .../syncope/common/lib/to/client/OIDCRPTO.java     | 168 +++++++++++++
 .../syncope/common/lib/to/client/SAML2SPTO.java    | 225 +++++++++++++++++
 .../syncope/common/lib/types/ClientAppType.java    |  28 +++
 .../syncope/common/lib/types/SAML2SPNameId.java    |  42 ++++
 .../common/rest/api/service/ClientAppService.java  | 134 +++++++++++
 .../apache/syncope/core/logic/ClientAppLogic.java  | 204 ++++++++++++++++
 .../rest/cxf/service/ClientAppServiceImpl.java     |  66 +++++
 .../core/persistence/api/dao/auth/OIDCRPDAO.java   |  45 ++++
 .../core/persistence/api/dao/auth/SAML2SPDAO.java  |  45 ++++
 .../persistence/api/entity/auth/ClientApp.java     |  56 +++++
 .../api/entity/auth/ClientAppUtils.java            |  28 +++
 .../api/entity/auth/ClientAppUtilsFactory.java     |  33 +++
 .../core/persistence/api/entity/auth/OIDCRP.java   |  52 ++++
 .../core/persistence/api/entity/auth/SAML2SP.java  |  76 ++++++
 .../persistence/jpa/dao/auth/JPAOIDCRPDAO.java     | 107 ++++++++
 .../persistence/jpa/dao/auth/JPASAML2SPDAO.java    | 107 ++++++++
 .../jpa/entity/auth/AbstractClientApp.java         | 131 ++++++++++
 .../jpa/entity/auth/JPAClientAppUtils.java         |  51 ++++
 .../jpa/entity/auth/JPAClientAppUtilsFactory.java  |  72 ++++++
 .../persistence/jpa/entity/auth/JPAOIDCRP.java     | 143 +++++++++++
 .../persistence/jpa/entity/auth/JPASAML2SP.java    | 204 ++++++++++++++++
 .../jpa/inner/AbstractClientAppTest.java           | 109 +++++++++
 .../core/persistence/jpa/inner/OIDCRPTest.java     |  82 +++++++
 .../core/persistence/jpa/inner/SAML2SPTest.java    |  80 ++++++
 .../provisioning/api/data/ClientAppDataBinder.java |  31 +++
 .../java/data/ClientAppDataBinderImpl.java         | 268 +++++++++++++++++++++
 .../apache/syncope/fit/core/ClientAppITCase.java   | 155 ++++++++++++
 28 files changed, 2896 insertions(+)

diff --git a/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/client/ClientAppTO.java b/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/client/ClientAppTO.java
new file mode 100644
index 0000000..ed8a6f0
--- /dev/null
+++ b/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/client/ClientAppTO.java
@@ -0,0 +1,154 @@
+/*
+ * 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.client;
+
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import io.swagger.v3.oas.annotations.media.Schema;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import org.apache.syncope.common.lib.BaseBean;
+import org.apache.syncope.common.lib.to.EntityTO;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlSeeAlso;
+import javax.xml.bind.annotation.XmlType;
+
+@XmlRootElement(name = "clientApp")
+@XmlType
+@XmlSeeAlso({ OIDCRPTO.class, SAML2SPTO.class })
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "@class")
+@JsonPropertyOrder(value = { "@class", "key", "name", "description", "authPolicy", "accessPolicy", "attReleasePolicy" })
+@Schema(subTypes = { OIDCRPTO.class, SAML2SPTO.class }, discriminatorProperty = "@class")
+public abstract class ClientAppTO extends BaseBean implements EntityTO {
+
+    private static final long serialVersionUID = 6577639976115661357L;
+
+    private String key;
+
+    private String name;
+
+    private Long clientAppId;
+
+    private String description;
+
+    private String authPolicy;
+
+    private String accessPolicy;
+
+    private String attrReleasePolicy;
+
+    public String getAttrReleasePolicy() {
+        return attrReleasePolicy;
+    }
+
+    public void setAttrReleasePolicy(final String attrReleasePolicy) {
+        this.attrReleasePolicy = attrReleasePolicy;
+    }
+
+    public String getAccessPolicy() {
+        return accessPolicy;
+    }
+
+    public void setAccessPolicy(final String accessPolicy) {
+        this.accessPolicy = accessPolicy;
+    }
+
+    public String getAuthPolicy() {
+        return authPolicy;
+    }
+
+    public void setAuthPolicy(final String authPolicy) {
+        this.authPolicy = authPolicy;
+    }
+
+    @Override
+    public String getKey() {
+        return key;
+    }
+
+    @Override
+    public void setKey(final String key) {
+        this.key = key;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(final String name) {
+        this.name = name;
+    }
+
+    public Long getClientAppId() {
+        return clientAppId;
+    }
+
+    public void setClientAppId(final Long clientAppId) {
+        this.clientAppId = clientAppId;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(final String description) {
+        this.description = description;
+    }
+
+    @Schema(name = "@class", required = true)
+    public abstract String getDiscriminator();
+
+    @Override
+    public int hashCode() {
+        return new HashCodeBuilder()
+                .appendSuper(super.hashCode())
+                .append(key)
+                .append(clientAppId)
+                .append(name)
+                .append(description)
+                .append(authPolicy)
+                .append(accessPolicy)
+                .append(attrReleasePolicy)
+                .toHashCode();
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (obj == this) {
+            return true;
+        }
+        if (obj.getClass() != getClass()) {
+            return false;
+        }
+        ClientAppTO rhs = (ClientAppTO) obj;
+        return new EqualsBuilder()
+                .appendSuper(super.equals(obj))
+                .append(this.key, rhs.key)
+                .append(this.clientAppId, rhs.clientAppId)
+                .append(this.name, rhs.name)
+                .append(this.description, rhs.description)
+                .append(this.authPolicy, rhs.authPolicy)
+                .append(this.accessPolicy, rhs.accessPolicy)
+                .append(this.attrReleasePolicy, rhs.attrReleasePolicy)
+                .isEquals();
+    }
+}
diff --git a/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/client/OIDCRPTO.java b/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/client/OIDCRPTO.java
new file mode 100644
index 0000000..987c534
--- /dev/null
+++ b/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/client/OIDCRPTO.java
@@ -0,0 +1,168 @@
+/*
+ * 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.client;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.util.ArrayList;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlTransient;
+import javax.xml.bind.annotation.XmlType;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlElementWrapper;
+import org.apache.syncope.common.lib.types.OIDCSubjectType;
+
+@XmlRootElement(name = "oidcrp")
+@XmlType
+@Schema(allOf = { ClientAppTO.class })
+public class OIDCRPTO extends ClientAppTO {
+
+    private static final long serialVersionUID = -6370888503924521351L;
+
+    private String clientId;
+
+    private String clientSecret;
+
+    private boolean signIdToken;
+
+    private String jwks;
+
+    private OIDCSubjectType subjectType;
+
+    private final List<String> redirectUris = new ArrayList<>();
+
+    private final Set<String> supportedGrantTypes = new HashSet<>();
+
+    private final Set<String> supportedResponseTypes = new HashSet<>();
+
+    @XmlTransient
+    @JsonProperty("@class")
+    @Schema(name = "@class", required = true,
+            example = "org.apache.syncope.common.lib.to.client.OIDCRPTO")
+    @Override
+    public String getDiscriminator() {
+        return getClass().getName();
+    }
+
+    public String getClientId() {
+        return clientId;
+    }
+
+    public void setClientId(final String clientId) {
+        this.clientId = clientId;
+    }
+
+    public String getClientSecret() {
+        return clientSecret;
+    }
+
+    public void setClientSecret(final String clientSecret) {
+        this.clientSecret = clientSecret;
+    }
+
+    @XmlElementWrapper(name = "redirectUris")
+    @XmlElement(name = "redirectUri")
+    @JsonProperty("redirectUris")
+    public List<String> getRedirectUris() {
+        return redirectUris;
+    }
+
+    @XmlElementWrapper(name = "supportedGrantTypes")
+    @XmlElement(name = "supportedGrantType")
+    @JsonProperty("supportedGrantTypes")
+    public Set<String> getSupportedGrantTypes() {
+        return supportedGrantTypes;
+    }
+
+    @XmlElementWrapper(name = "supportedResponseTypes")
+    @XmlElement(name = "supportedResponseType")
+    @JsonProperty("supportedResponseTypes")
+    public Set<String> getSupportedResponseTypes() {
+        return supportedResponseTypes;
+    }
+
+    public boolean isSignIdToken() {
+        return signIdToken;
+    }
+
+    public void setSignIdToken(final boolean signIdToken) {
+        this.signIdToken = signIdToken;
+    }
+
+    public String getJwks() {
+        return jwks;
+    }
+
+    public void setJwks(final String jwks) {
+        this.jwks = jwks;
+    }
+
+    public OIDCSubjectType getSubjectType() {
+        return subjectType;
+    }
+
+    public void setSubjectType(final OIDCSubjectType subjectType) {
+        this.subjectType = subjectType;
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (obj == this) {
+            return true;
+        }
+        if (obj.getClass() != getClass()) {
+            return false;
+        }
+        OIDCRPTO rhs = (OIDCRPTO) obj;
+        return new EqualsBuilder()
+                .appendSuper(super.equals(obj))
+                .append(this.clientId, rhs.clientId)
+                .append(this.clientSecret, rhs.clientSecret)
+                .append(this.redirectUris, rhs.redirectUris)
+                .append(this.supportedGrantTypes, rhs.supportedGrantTypes)
+                .append(this.supportedResponseTypes, rhs.supportedResponseTypes)
+                .append(this.signIdToken, rhs.signIdToken)
+                .append(this.jwks, rhs.jwks)
+                .append(this.subjectType, rhs.subjectType)
+                .isEquals();
+    }
+
+    @Override
+    public int hashCode() {
+        return new HashCodeBuilder()
+                .appendSuper(super.hashCode())
+                .append(clientId)
+                .append(clientSecret)
+                .append(redirectUris)
+                .append(supportedGrantTypes)
+                .append(supportedResponseTypes)
+                .append(signIdToken)
+                .append(jwks)
+                .append(subjectType)
+                .toHashCode();
+    }
+}
diff --git a/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/client/SAML2SPTO.java b/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/client/SAML2SPTO.java
new file mode 100644
index 0000000..d2cfac5
--- /dev/null
+++ b/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/client/SAML2SPTO.java
@@ -0,0 +1,225 @@
+/*
+ * 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.client;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import javax.xml.bind.annotation.XmlRootElement;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import javax.xml.bind.annotation.XmlTransient;
+import javax.xml.bind.annotation.XmlType;
+import org.apache.syncope.common.lib.types.SAML2SPNameId;
+
+@XmlRootElement(name = "saml2SP")
+@XmlType
+@Schema(allOf = { ClientAppTO.class })
+public class SAML2SPTO extends ClientAppTO {
+
+    private static final long serialVersionUID = -6370888503924521351L;
+
+    private String entityId;
+
+    private String metadataLocation;
+
+    private String metadataSignatureLocation;
+
+    private boolean signAssertions;
+
+    private boolean signResponses;
+
+    private boolean encryptionOptional;
+
+    private boolean encryptAssertions;
+
+    private String requiredAuthenticationContextClass;
+
+    private SAML2SPNameId requiredNameIdFormat;
+
+    private Integer skewAllowance;
+
+    private String nameIdQualifier;
+
+    private String assertionAudiences;
+
+    private String serviceProviderNameIdQualifier;
+
+    @XmlTransient
+    @JsonProperty("@class")
+    @Schema(name = "@class", required = true,
+            example = "org.apache.syncope.common.lib.to.client.SAML2SPTO")
+    @Override
+    public String getDiscriminator() {
+        return getClass().getName();
+    }
+
+    public String getEntityId() {
+        return entityId;
+    }
+
+    public void setEntityId(final String entityId) {
+        this.entityId = entityId;
+    }
+
+    public String getMetadataLocation() {
+        return metadataLocation;
+    }
+
+    public void setMetadataLocation(final String metadataLocation) {
+        this.metadataLocation = metadataLocation;
+    }
+
+    public String getMetadataSignatureLocation() {
+        return metadataSignatureLocation;
+    }
+
+    public void setMetadataSignatureLocation(final String metadataSignatureLocation) {
+        this.metadataSignatureLocation = metadataSignatureLocation;
+    }
+
+    public boolean isSignAssertions() {
+        return signAssertions;
+    }
+
+    public void setSignAssertions(final boolean signAssertions) {
+        this.signAssertions = signAssertions;
+    }
+
+    public boolean isSignResponses() {
+        return signResponses;
+    }
+
+    public void setSignResponses(final boolean signResponses) {
+        this.signResponses = signResponses;
+    }
+
+    public boolean isEncryptionOptional() {
+        return encryptionOptional;
+    }
+
+    public void setEncryptionOptional(final boolean encryptionOptional) {
+        this.encryptionOptional = encryptionOptional;
+    }
+
+    public boolean isEncryptAssertions() {
+        return encryptAssertions;
+    }
+
+    public void setEncryptAssertions(final boolean encryptAssertions) {
+        this.encryptAssertions = encryptAssertions;
+    }
+
+    public String getRequiredAuthenticationContextClass() {
+        return requiredAuthenticationContextClass;
+    }
+
+    public void setRequiredAuthenticationContextClass(final String requiredAuthenticationContextClass) {
+        this.requiredAuthenticationContextClass = requiredAuthenticationContextClass;
+    }
+
+    public SAML2SPNameId getRequiredNameIdFormat() {
+        return requiredNameIdFormat;
+    }
+
+    public void setRequiredNameIdFormat(final SAML2SPNameId requiredNameIdFormat) {
+        this.requiredNameIdFormat = requiredNameIdFormat;
+    }
+
+    public Integer getSkewAllowance() {
+        return skewAllowance;
+    }
+
+    public void setSkewAllowance(final Integer skewAllowance) {
+        this.skewAllowance = skewAllowance;
+    }
+
+    public String getNameIdQualifier() {
+        return nameIdQualifier;
+    }
+
+    public void setNameIdQualifier(final String nameIdQualifier) {
+        this.nameIdQualifier = nameIdQualifier;
+    }
+
+    public String getAssertionAudiences() {
+        return assertionAudiences;
+    }
+
+    public void setAssertionAudiences(final String assertionAudiences) {
+        this.assertionAudiences = assertionAudiences;
+    }
+
+    public String getServiceProviderNameIdQualifier() {
+        return serviceProviderNameIdQualifier;
+    }
+
+    public void setServiceProviderNameIdQualifier(final String serviceProviderNameIdQualifier) {
+        this.serviceProviderNameIdQualifier = serviceProviderNameIdQualifier;
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (obj == this) {
+            return true;
+        }
+        if (obj.getClass() != getClass()) {
+            return false;
+        }
+        SAML2SPTO rhs = (SAML2SPTO) obj;
+        return new EqualsBuilder()
+                .appendSuper(super.equals(obj))
+                .append(this.entityId, rhs.entityId)
+                .append(this.metadataLocation, rhs.metadataLocation)
+                .append(this.metadataSignatureLocation, rhs.metadataSignatureLocation)
+                .append(this.signAssertions, rhs.signAssertions)
+                .append(this.signResponses, rhs.signResponses)
+                .append(this.encryptionOptional, rhs.encryptionOptional)
+                .append(this.encryptAssertions, rhs.encryptAssertions)
+                .append(this.requiredAuthenticationContextClass, rhs.requiredAuthenticationContextClass)
+                .append(this.requiredNameIdFormat, rhs.requiredNameIdFormat)
+                .append(this.skewAllowance, rhs.skewAllowance)
+                .append(this.nameIdQualifier, rhs.nameIdQualifier)
+                .append(this.assertionAudiences, rhs.assertionAudiences)
+                .append(this.serviceProviderNameIdQualifier, rhs.serviceProviderNameIdQualifier)
+                .isEquals();
+    }
+
+    @Override
+    public int hashCode() {
+        return new HashCodeBuilder()
+                .appendSuper(super.hashCode())
+                .append(entityId)
+                .append(metadataLocation)
+                .append(metadataSignatureLocation)
+                .append(signAssertions)
+                .append(signResponses)
+                .append(encryptionOptional)
+                .append(encryptAssertions)
+                .append(requiredAuthenticationContextClass)
+                .append(requiredNameIdFormat)
+                .append(skewAllowance)
+                .append(nameIdQualifier)
+                .append(assertionAudiences)
+                .append(serviceProviderNameIdQualifier)
+                .toHashCode();
+    }
+}
diff --git a/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/ClientAppType.java b/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/ClientAppType.java
new file mode 100644
index 0000000..7f90159
--- /dev/null
+++ b/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/ClientAppType.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.common.lib.types;
+
+import javax.xml.bind.annotation.XmlEnum;
+
+@XmlEnum
+public enum ClientAppType {
+    SAML2SP,
+    OIDCRP;
+
+}
diff --git a/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/SAML2SPNameId.java b/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/SAML2SPNameId.java
new file mode 100644
index 0000000..d75a68d
--- /dev/null
+++ b/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/SAML2SPNameId.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.common.lib.types;
+
+import javax.xml.bind.annotation.XmlEnum;
+
+@XmlEnum
+public enum SAML2SPNameId {
+
+    EMAIL_ADDRESS("urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"),
+    UNSPECIFIED("urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"),
+    ENTITY("urn:oasis:names:tc:SAML:2.0:nameid-format:entity"),
+    PERSISTENT("urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"),
+    TRANSIENT("urn:oasis:names:tc:SAML:2.0:nameid-format:transient"),
+    ENCRYPTED("urn:oasis:names:tc:SAML:2.0:nameid-format:encrypted");
+
+    private final String nameId;
+
+    SAML2SPNameId(final String nameId) {
+        this.nameId = nameId;
+    }
+
+    public String getNameId() {
+        return nameId;
+    }
+}
diff --git a/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/ClientAppService.java b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/ClientAppService.java
new file mode 100644
index 0000000..c2e2398
--- /dev/null
+++ b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/ClientAppService.java
@@ -0,0 +1,134 @@
+/*
+ * 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.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 java.util.List;
+import javax.validation.constraints.NotNull;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+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.client.ClientAppTO;
+import org.apache.syncope.common.lib.types.ClientAppType;
+import org.apache.syncope.common.rest.api.RESTHeaders;
+
+/**
+ * REST operations for client applications.
+ */
+@Tag(name = "ClientApps")
+@SecurityRequirements({
+    @SecurityRequirement(name = "BasicAuthentication"),
+    @SecurityRequirement(name = "Bearer") })
+@Path("clientApps")
+public interface ClientAppService extends JAXRSService {
+
+    /**
+     * Returns the client app matching the given key.
+     *
+     * @param type client app type
+     * @param key key of requested client app
+     * @param <T> response type (extending ClientAppTO)
+     * @return client app with matching id
+     */
+    @GET
+    @Path("{type}/{key}")
+    @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML })
+    <T extends ClientAppTO> T read(
+            @NotNull @PathParam("type") ClientAppType type,
+            @NotNull @PathParam("key") String key);
+
+    /**
+     * Returns a list of client apps of the matching type.
+     *
+     * @param type Type selector for requested client apps
+     * @param <T> response type (extending ClientAppTO)
+     * @return list of client apps with matching type
+     */
+    @GET
+    @Path("{type}")
+    @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML })
+    <T extends ClientAppTO> List<T> list(@NotNull @PathParam("type") ClientAppType type);
+
+    /**
+     * Create a new client app.
+     *
+     * @param type client app type
+     * @param clientAppTO ClientApp to be created (needs to match type)
+     * @return Response object featuring Location header of created client app
+     */
+    @ApiResponses(
+            @ApiResponse(responseCode = "201",
+                    description = "ClientApp 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") }))
+    @POST
+    @Path("{type}")
+    @Consumes({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML })
+    @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML })
+    Response create(@NotNull @PathParam("type") ClientAppType type, @NotNull ClientAppTO clientAppTO);
+
+    /**
+     * Updates client app matching the given key.
+     *
+     * @param type client app type
+     * @param clientAppTO ClientApp to replace existing client app
+     */
+    @Parameter(name = "key", description = "ClientApp's key", in = ParameterIn.PATH, schema =
+            @Schema(type = "string"))
+    @ApiResponses(
+            @ApiResponse(responseCode = "204", description = "Operation was successful"))
+    @PUT
+    @Path("{type}/{key}")
+    @Consumes({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML })
+    @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML })
+    void update(@NotNull @PathParam("type") ClientAppType type, @NotNull ClientAppTO clientAppTO);
+
+    /**
+     * Delete client app matching the given key.
+     *
+     * @param type client app type
+     * @param key key of client app to be deleted
+     */
+    @ApiResponses(
+            @ApiResponse(responseCode = "204", description = "Operation was successful"))
+    @DELETE
+    @Path("{type}/{key}")
+    @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML })
+    void delete(@NotNull @PathParam("type") ClientAppType type, @NotNull @PathParam("key") String key);
+}
diff --git a/core/am/logic/src/main/java/org/apache/syncope/core/logic/ClientAppLogic.java b/core/am/logic/src/main/java/org/apache/syncope/core/logic/ClientAppLogic.java
new file mode 100644
index 0000000..6e27383
--- /dev/null
+++ b/core/am/logic/src/main/java/org/apache/syncope/core/logic/ClientAppLogic.java
@@ -0,0 +1,204 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.core.logic;
+
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.to.client.ClientAppTO;
+import org.apache.syncope.common.lib.types.AMEntitlement;
+import org.apache.syncope.common.lib.types.ClientAppType;
+import org.apache.syncope.common.lib.types.ClientExceptionType;
+import org.apache.syncope.core.persistence.api.dao.NotFoundException;
+import org.apache.syncope.core.persistence.api.dao.auth.OIDCRPDAO;
+import org.apache.syncope.core.persistence.api.dao.auth.SAML2SPDAO;
+import org.apache.syncope.core.persistence.api.entity.auth.ClientApp;
+import org.apache.syncope.core.persistence.api.entity.auth.ClientAppUtils;
+import org.apache.syncope.core.persistence.api.entity.auth.ClientAppUtilsFactory;
+import org.apache.syncope.core.persistence.api.entity.auth.OIDCRP;
+import org.apache.syncope.core.persistence.api.entity.auth.SAML2SP;
+import org.apache.syncope.core.provisioning.api.data.ClientAppDataBinder;
+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 ClientAppLogic extends AbstractTransactionalLogic<ClientAppTO> {
+
+    @Autowired
+    private ClientAppUtilsFactory clientAppUtilsFactory;
+
+    @Autowired
+    private ClientAppDataBinder binder;
+
+    @Autowired
+    private SAML2SPDAO saml2spDAO;
+
+    @Autowired
+    private OIDCRPDAO oidcrpDAO;
+
+    @PreAuthorize("hasRole('" + AMEntitlement.CLIENTAPP_LIST + "')")
+    public <T extends ClientAppTO> List<T> list(final ClientAppType type) {
+        Stream<T> stream;
+
+        switch (type) {
+            case OIDCRP:
+                stream = oidcrpDAO.findAll().stream().map(binder::getClientAppTO);
+                break;
+
+            case SAML2SP:
+            default:
+                stream = saml2spDAO.findAll().stream().map(binder::getClientAppTO);
+        }
+
+        return stream.collect(Collectors.toList());
+    }
+
+    private void checkType(final ClientAppType type, final ClientAppUtils clientAppUtils) {
+        if (clientAppUtils.getType() != type) {
+            SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidRequest);
+            sce.getElements().add("Found " + type + ", expected " + clientAppUtils.getType());
+            throw sce;
+        }
+    }
+
+    @PreAuthorize("hasRole('" + AMEntitlement.CLIENTAPP_READ + "')")
+    @Transactional(readOnly = true)
+    public <T extends ClientAppTO> T read(final ClientAppType type, final String key) {
+        switch (type) {
+            case OIDCRP:
+                OIDCRP oidcrp = oidcrpDAO.find(key);
+                if (oidcrp == null) {
+                    throw new NotFoundException("Client app " + key + " not found");
+                }
+
+                checkType(type, clientAppUtilsFactory.getInstance(oidcrp));
+
+                return binder.getClientAppTO(oidcrp);
+
+            case SAML2SP:
+            default:
+                SAML2SP saml2sp = saml2spDAO.find(key);
+                if (saml2sp == null) {
+                    throw new NotFoundException("Client app " + key + " not found");
+                }
+
+                checkType(type, clientAppUtilsFactory.getInstance(saml2sp));
+
+                return binder.getClientAppTO(saml2sp);
+        }
+    }
+
+    @PreAuthorize("hasRole('" + AMEntitlement.CLIENTAPP_CREATE + "')")
+    public ClientAppTO create(final ClientAppType type, final ClientAppTO clientAppTO) {
+        checkType(type, clientAppUtilsFactory.getInstance(clientAppTO));
+
+        switch (type) {
+            case OIDCRP:
+                return binder.getClientAppTO(oidcrpDAO.save(binder.create(clientAppTO)));
+
+            case SAML2SP:
+            default:
+                return binder.getClientAppTO(saml2spDAO.save(binder.create(clientAppTO)));
+        }
+    }
+
+    @PreAuthorize("hasRole('" + AMEntitlement.CLIENTAPP_UPDATE + "')")
+    public void update(final ClientAppType type, final ClientAppTO clientAppTO) {
+        checkType(type, clientAppUtilsFactory.getInstance(clientAppTO));
+
+        switch (type) {
+            case OIDCRP:
+                OIDCRP oidcrp = oidcrpDAO.find(clientAppTO.getKey());
+                if (oidcrp == null) {
+                    throw new NotFoundException("Client app " + clientAppTO.getKey() + " not found");
+                }
+                binder.update(oidcrp, clientAppTO);
+                oidcrpDAO.save(oidcrp);
+                break;
+
+            case SAML2SP:
+            default:
+                SAML2SP saml2sp = saml2spDAO.find(clientAppTO.getKey());
+                if (saml2sp == null) {
+                    throw new NotFoundException("Client app " + clientAppTO.getKey() + " not found");
+                }
+                binder.update(saml2sp, clientAppTO);
+                saml2spDAO.save(saml2sp);
+        }
+    }
+
+    @PreAuthorize("hasRole('" + AMEntitlement.CLIENTAPP_DELETE + "')")
+    public void delete(final ClientAppType type, final String key) {
+        switch (type) {
+            case OIDCRP:
+                OIDCRP oidcrp = oidcrpDAO.find(key);
+                if (oidcrp == null) {
+                    throw new NotFoundException("Client app " + key + " not found");
+                }
+                oidcrpDAO.delete(oidcrp);
+                break;
+
+            case SAML2SP:
+            default:
+                SAML2SP saml2sp = saml2spDAO.find(key);
+                if (saml2sp == null) {
+                    throw new NotFoundException("Client app " + key + " not found");
+                }
+                saml2spDAO.delete(saml2sp);
+        }
+    }
+
+    @Override
+    protected ClientAppTO resolveReference(final Method method, final Object... args)
+            throws UnresolvedReferenceException {
+
+        String key = null;
+
+        if (ArrayUtils.isNotEmpty(args)) {
+            for (int i = 0; key == null && i < args.length; i++) {
+                if (args[i] instanceof String) {
+                    key = (String) args[i];
+                } else if (args[i] instanceof ClientAppTO) {
+                    key = ((ClientAppTO) args[i]).getKey();
+                }
+            }
+        }
+
+        if (key != null) {
+            try {
+                ClientApp clientApp = saml2spDAO.find(key);
+                if (clientApp == null) {
+                    clientApp = oidcrpDAO.find(key);
+                }
+
+                return binder.getClientAppTO(clientApp);
+            } 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/ClientAppServiceImpl.java b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ClientAppServiceImpl.java
new file mode 100644
index 0000000..e2e461e
--- /dev/null
+++ b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ClientAppServiceImpl.java
@@ -0,0 +1,66 @@
+/*
+ * 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 java.util.List;
+import javax.ws.rs.core.Response;
+import org.apache.syncope.common.lib.to.client.ClientAppTO;
+import org.apache.syncope.common.lib.types.ClientAppType;
+import org.apache.syncope.common.rest.api.RESTHeaders;
+import org.apache.syncope.common.rest.api.service.ClientAppService;
+import org.apache.syncope.core.logic.ClientAppLogic;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class ClientAppServiceImpl extends AbstractServiceImpl implements ClientAppService {
+
+    @Autowired
+    private ClientAppLogic logic;
+
+    @Override
+    public Response create(final ClientAppType type, final ClientAppTO clientAppTO) {
+        ClientAppTO appTO = logic.create(type, clientAppTO);
+        URI location = uriInfo.getAbsolutePathBuilder().path(appTO.getKey()).build();
+        return Response.created(location).
+                header(RESTHeaders.RESOURCE_KEY, appTO.getKey()).
+                build();
+    }
+
+    @Override
+    public <T extends ClientAppTO> List<T> list(final ClientAppType type) {
+        return logic.list(type);
+    }
+
+    @Override
+    public <T extends ClientAppTO> T read(final ClientAppType type, final String key) {
+        return logic.read(type, key);
+    }
+
+    @Override
+    public void update(final ClientAppType type, final ClientAppTO clientAppTO) {
+        logic.update(type, clientAppTO);
+    }
+
+    @Override
+    public void delete(final ClientAppType type, final String key) {
+        logic.delete(type, key);
+    }
+}
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/auth/OIDCRPDAO.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/auth/OIDCRPDAO.java
new file mode 100644
index 0000000..517c92b
--- /dev/null
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/auth/OIDCRPDAO.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.core.persistence.api.dao.auth;
+
+import org.apache.syncope.core.persistence.api.dao.DAO;
+
+import java.util.List;
+import org.apache.syncope.core.persistence.api.entity.auth.OIDCRP;
+
+public interface OIDCRPDAO extends DAO<OIDCRP> {
+
+    OIDCRP find(String key);
+
+    OIDCRP findByClientAppId(Long clientAppId);
+
+    OIDCRP findByName(String name);
+
+    OIDCRP findByClientId(String clientId);
+
+    List<OIDCRP> findAll();
+
+    OIDCRP save(OIDCRP clientApp);
+
+    void delete(String key);
+
+    void deleteByClientId(String clientId);
+
+    void delete(OIDCRP clientApp);
+}
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/auth/SAML2SPDAO.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/auth/SAML2SPDAO.java
new file mode 100644
index 0000000..2d2fdbd
--- /dev/null
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/auth/SAML2SPDAO.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.core.persistence.api.dao.auth;
+
+import org.apache.syncope.core.persistence.api.dao.DAO;
+
+import java.util.List;
+import org.apache.syncope.core.persistence.api.entity.auth.SAML2SP;
+
+public interface SAML2SPDAO extends DAO<SAML2SP> {
+
+    SAML2SP find(String key);
+
+    SAML2SP findByClientAppId(Long clientAppId);
+
+    SAML2SP findByName(String name);
+
+    SAML2SP findByEntityId(String clientId);
+
+    List<SAML2SP> findAll();
+
+    SAML2SP save(SAML2SP clientApp);
+
+    void delete(String key);
+
+    void deleteByEntityId(String entityId);
+
+    void delete(SAML2SP clientApp);
+}
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/auth/ClientApp.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/auth/ClientApp.java
new file mode 100644
index 0000000..70bf7b7
--- /dev/null
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/auth/ClientApp.java
@@ -0,0 +1,56 @@
+/*
+ * 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;
+import org.apache.syncope.core.persistence.api.entity.Realm;
+import org.apache.syncope.core.persistence.api.entity.policy.AccessPolicy;
+import org.apache.syncope.core.persistence.api.entity.policy.AttrReleasePolicy;
+import org.apache.syncope.core.persistence.api.entity.policy.AuthPolicy;
+
+public interface ClientApp extends Entity {
+
+    String getName();
+
+    void setName(String name);
+
+    Long getClientAppId();
+
+    void setClientAppId(Long clientAppId);
+
+    String getDescription();
+
+    void setDescription(String description);
+
+    AuthPolicy getAuthPolicy();
+
+    void setAuthPolicy(AuthPolicy policy);
+
+    AccessPolicy getAccessPolicy();
+
+    void setAccessPolicy(AccessPolicy policy);
+
+    AttrReleasePolicy getAttrReleasePolicy();
+
+    void setAttrReleasePolicy(AttrReleasePolicy policy);
+
+    Realm getRealm();
+
+    void setRealm(Realm realm);
+}
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/auth/ClientAppUtils.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/auth/ClientAppUtils.java
new file mode 100644
index 0000000..7db5f11
--- /dev/null
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/auth/ClientAppUtils.java
@@ -0,0 +1,28 @@
+/*
+ * 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.common.lib.types.ClientAppType;
+
+public interface ClientAppUtils {
+
+    ClientAppType getType();
+
+    Class<? extends ClientApp> clientAppClass();
+}
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/auth/ClientAppUtilsFactory.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/auth/ClientAppUtilsFactory.java
new file mode 100644
index 0000000..6777d9d
--- /dev/null
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/auth/ClientAppUtilsFactory.java
@@ -0,0 +1,33 @@
+/*
+ * 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.common.lib.to.client.ClientAppTO;
+import org.apache.syncope.common.lib.types.ClientAppType;
+
+public interface ClientAppUtilsFactory {
+
+    ClientAppUtils getInstance(ClientAppType type);
+
+    ClientAppUtils getInstance(ClientApp clientApp);
+
+    ClientAppUtils getInstance(Class<? extends ClientAppTO> clientAppClass);
+
+    ClientAppUtils getInstance(ClientAppTO clientAppTO);
+}
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/auth/OIDCRP.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/auth/OIDCRP.java
new file mode 100644
index 0000000..0a72461
--- /dev/null
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/auth/OIDCRP.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.core.persistence.api.entity.auth;
+
+import java.util.List;
+import java.util.Set;
+import org.apache.syncope.common.lib.types.OIDCSubjectType;
+
+public interface OIDCRP extends ClientApp {
+
+    void setClientId(String id);
+
+    String getClientId();
+
+    void setClientSecret(String secret);
+
+    String getClientSecret();
+
+    List<String> getRedirectUris();
+
+    Set<String> getSupportedGrantTypes();
+
+    Set<String> getSupportedResponseTypes();
+
+    boolean isSignIdToken();
+
+    void setSignIdToken(boolean signIdToken);
+
+    String getJwks();
+
+    void setJwks(String jwks);
+
+    OIDCSubjectType getSubjectType();
+
+    void setSubjectType(OIDCSubjectType subjectType);
+}
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/auth/SAML2SP.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/auth/SAML2SP.java
new file mode 100644
index 0000000..2103b5d
--- /dev/null
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/auth/SAML2SP.java
@@ -0,0 +1,76 @@
+/*
+ * 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.common.lib.types.SAML2SPNameId;
+
+public interface SAML2SP extends ClientApp {
+
+    String getEntityId();
+
+    void setEntityId(String id);
+
+    String getMetadataLocation();
+
+    void setMetadataLocation(String location);
+
+    void setMetadataSignatureLocation(String location);
+
+    String getMetadataSignatureLocation();
+
+    void setSignAssertions(boolean location);
+
+    boolean isSignAssertions();
+
+    void setSignResponses(boolean location);
+
+    boolean isSignResponses();
+
+    void setEncryptionOptional(boolean location);
+
+    boolean isEncryptionOptional();
+
+    void setEncryptAssertions(boolean location);
+
+    boolean isEncryptAssertions();
+
+    void setRequiredAuthenticationContextClass(String location);
+
+    String getRequiredAuthenticationContextClass();
+
+    void setRequiredNameIdFormat(SAML2SPNameId location);
+
+    SAML2SPNameId getRequiredNameIdFormat();
+
+    void setSkewAllowance(Integer location);
+
+    Integer getSkewAllowance();
+
+    void setNameIdQualifier(String location);
+
+    String getNameIdQualifier();
+
+    void setAssertionAudiences(String location);
+
+    String getAssertionAudiences();
+
+    void setServiceProviderNameIdQualifier(String location);
+
+    String getServiceProviderNameIdQualifier();
+}
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/auth/JPAOIDCRPDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/auth/JPAOIDCRPDAO.java
new file mode 100644
index 0000000..bf6b32a
--- /dev/null
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/auth/JPAOIDCRPDAO.java
@@ -0,0 +1,107 @@
+/*
+ * 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 java.util.List;
+import javax.persistence.NoResultException;
+import javax.persistence.TypedQuery;
+import org.apache.syncope.core.persistence.jpa.dao.AbstractDAO;
+import org.apache.syncope.core.persistence.jpa.entity.auth.JPAOIDCRP;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+import org.apache.syncope.core.persistence.api.dao.auth.OIDCRPDAO;
+import org.apache.syncope.core.persistence.api.entity.auth.OIDCRP;
+
+@Repository
+public class JPAOIDCRPDAO extends AbstractDAO<OIDCRP> implements OIDCRPDAO {
+
+    @Override
+    public OIDCRP find(final String key) {
+        return entityManager().find(JPAOIDCRP.class, key);
+    }
+
+    private OIDCRP find(final String column, final Object value) {
+        TypedQuery<OIDCRP> query = entityManager().createQuery(
+                "SELECT e FROM " + JPAOIDCRP.class.getSimpleName() + " e WHERE e." + column + "=:value",
+                OIDCRP.class);
+        query.setParameter("value", value);
+
+        OIDCRP result = null;
+        try {
+            result = query.getSingleResult();
+        } catch (final NoResultException e) {
+            LOG.debug("No OIDCRP found with " + column + " {}", value, e);
+        }
+
+        return result;
+    }
+
+    @Override
+    public OIDCRP findByClientAppId(final Long clientAppId) {
+        return find("clientAppId", clientAppId);
+    }
+
+    @Override
+    public OIDCRP findByName(final String name) {
+        return find("name", name);
+    }
+
+    @Override
+    public OIDCRP findByClientId(final String clientId) {
+        return find("clientId", clientId);
+    }
+
+    @Transactional(readOnly = true)
+    @Override
+    public List<OIDCRP> findAll() {
+        TypedQuery<OIDCRP> query = entityManager().createQuery(
+                "SELECT e FROM " + JPAOIDCRP.class.getSimpleName() + " e", OIDCRP.class);
+
+        return query.getResultList();
+    }
+
+    @Override
+    public OIDCRP save(final OIDCRP clientApp) {
+        return entityManager().merge(clientApp);
+    }
+
+    @Override
+    public void delete(final String key) {
+        OIDCRP rpTO = find(key);
+        if (rpTO == null) {
+            return;
+        }
+
+        delete(rpTO);
+    }
+
+    @Override
+    public void deleteByClientId(final String clientId) {
+        OIDCRP rpTO = findByClientId(clientId);
+        if (rpTO == null) {
+            return;
+        }
+        delete(rpTO);
+    }
+
+    @Override
+    public void delete(final OIDCRP clientApp) {
+        entityManager().remove(clientApp);
+    }
+}
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/auth/JPASAML2SPDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/auth/JPASAML2SPDAO.java
new file mode 100644
index 0000000..e8ce293
--- /dev/null
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/auth/JPASAML2SPDAO.java
@@ -0,0 +1,107 @@
+/*
+ * 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.jpa.dao.AbstractDAO;
+import org.apache.syncope.core.persistence.jpa.entity.auth.JPASAML2SP;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+import javax.persistence.NoResultException;
+import javax.persistence.TypedQuery;
+import java.util.List;
+import org.apache.syncope.core.persistence.api.dao.auth.SAML2SPDAO;
+import org.apache.syncope.core.persistence.api.entity.auth.SAML2SP;
+
+@Repository
+public class JPASAML2SPDAO extends AbstractDAO<SAML2SP> implements SAML2SPDAO {
+
+    @Override
+    public SAML2SP find(final String key) {
+        return entityManager().find(JPASAML2SP.class, key);
+    }
+
+    private SAML2SP find(final String column, final Object value) {
+        TypedQuery<SAML2SP> query = entityManager().createQuery(
+                "SELECT e FROM " + JPASAML2SP.class.getSimpleName() + " e WHERE e." + column + "=:value",
+                SAML2SP.class);
+        query.setParameter("value", value);
+
+        SAML2SP result = null;
+        try {
+            result = query.getSingleResult();
+        } catch (final NoResultException e) {
+            LOG.debug("No SAML2SP found with " + column + " {}", value, e);
+        }
+
+        return result;
+    }
+
+    @Override
+    public SAML2SP findByClientAppId(final Long clientAppId) {
+        return find("clientAppId", clientAppId);
+    }
+
+    @Override
+    public SAML2SP findByName(final String name) {
+        return find("name", name);
+    }
+
+    @Override
+    public SAML2SP findByEntityId(final String entityId) {
+        return find("entityId", entityId);
+    }
+
+    @Transactional(readOnly = true)
+    @Override
+    public List<SAML2SP> findAll() {
+        TypedQuery<SAML2SP> query = entityManager().createQuery(
+                "SELECT e FROM " + JPASAML2SP.class.getSimpleName() + " e", SAML2SP.class);
+
+        return query.getResultList();
+    }
+
+    @Override
+    public SAML2SP save(final SAML2SP clientApp) {
+        return entityManager().merge(clientApp);
+    }
+
+    @Override
+    public void delete(final String key) {
+        SAML2SP policy = find(key);
+        if (policy == null) {
+            return;
+        }
+
+        delete(policy);
+    }
+
+    @Override
+    public void deleteByEntityId(final String entityId) {
+        SAML2SP app = findByEntityId(entityId);
+        if (app == null) {
+            return;
+        }
+        delete(app);
+    }
+
+    @Override
+    public void delete(final SAML2SP clientApp) {
+        entityManager().remove(clientApp);
+    }
+}
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/auth/AbstractClientApp.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/auth/AbstractClientApp.java
new file mode 100644
index 0000000..e815c0f
--- /dev/null
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/auth/AbstractClientApp.java
@@ -0,0 +1,131 @@
+/*
+ * 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.Realm;
+import org.apache.syncope.core.persistence.api.entity.auth.ClientApp;
+import org.apache.syncope.core.persistence.api.entity.policy.AttrReleasePolicy;
+import org.apache.syncope.core.persistence.api.entity.policy.AccessPolicy;
+import org.apache.syncope.core.persistence.jpa.entity.AbstractGeneratedKeyEntity;
+import org.apache.syncope.core.persistence.jpa.entity.JPARealm;
+import org.apache.syncope.core.persistence.jpa.entity.policy.JPAAttrReleasePolicy;
+import org.apache.syncope.core.persistence.jpa.entity.policy.JPAAuthPolicy;
+import org.apache.syncope.core.persistence.jpa.entity.policy.JPAAccessPolicy;
+import javax.persistence.Column;
+import javax.persistence.FetchType;
+import javax.persistence.ManyToOne;
+import javax.persistence.MappedSuperclass;
+import org.apache.syncope.core.persistence.api.entity.policy.AuthPolicy;
+
+@MappedSuperclass
+public class AbstractClientApp extends AbstractGeneratedKeyEntity implements ClientApp {
+
+    private static final long serialVersionUID = 7422422526695279794L;
+
+    @Column(unique = true, nullable = false)
+    private String name;
+
+    @Column(unique = true, nullable = false)
+    private Long clientAppId;
+
+    @Column
+    private String description;
+
+    @ManyToOne(fetch = FetchType.EAGER)
+    private JPARealm realm;
+
+    @ManyToOne(fetch = FetchType.EAGER)
+    private JPAAuthPolicy authPolicy;
+
+    @ManyToOne(fetch = FetchType.EAGER)
+    private JPAAccessPolicy accessPolicy;
+
+    @ManyToOne(fetch = FetchType.EAGER)
+    private JPAAttrReleasePolicy attrReleasePolicy;
+
+    public Long getClientAppId() {
+        return clientAppId;
+    }
+
+    public void setClientAppId(final Long clientAppId) {
+        this.clientAppId = clientAppId;
+    }
+
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    @Override
+    public void setName(final String name) {
+        this.name = name;
+    }
+
+    @Override
+    public String getDescription() {
+        return description;
+    }
+
+    @Override
+    public void setDescription(final String description) {
+        this.description = description;
+    }
+
+    @Override
+    public JPAAuthPolicy getAuthPolicy() {
+        return authPolicy;
+    }
+
+    @Override
+    public void setAuthPolicy(final AuthPolicy authPolicy) {
+        checkType(authPolicy, JPAAuthPolicy.class);
+        this.authPolicy = (JPAAuthPolicy) authPolicy;
+    }
+
+    public JPAAccessPolicy getAccessPolicy() {
+        return accessPolicy;
+    }
+
+    public void setAccessPolicy(final AccessPolicy accessPolicy) {
+        checkType(accessPolicy, JPAAccessPolicy.class);
+        this.accessPolicy = (JPAAccessPolicy) accessPolicy;
+    }
+
+    @Override
+    public AttrReleasePolicy getAttrReleasePolicy() {
+        return this.attrReleasePolicy;
+    }
+
+    @Override
+    public void setAttrReleasePolicy(final AttrReleasePolicy policy) {
+        checkType(policy, JPAAccessPolicy.class);
+        this.attrReleasePolicy = (JPAAttrReleasePolicy) policy;
+    }
+
+    @Override
+    public Realm getRealm() {
+        return realm;
+    }
+
+    @Override
+    public void setRealm(final Realm realm) {
+        checkType(realm, JPARealm.class);
+        this.realm = (JPARealm) realm;
+    }
+}
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/auth/JPAClientAppUtils.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/auth/JPAClientAppUtils.java
new file mode 100644
index 0000000..ced3c6a
--- /dev/null
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/auth/JPAClientAppUtils.java
@@ -0,0 +1,51 @@
+/*
+ * 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.common.lib.types.ClientAppType;
+import org.apache.syncope.core.persistence.api.entity.auth.ClientApp;
+import org.apache.syncope.core.persistence.api.entity.auth.ClientAppUtils;
+import org.apache.syncope.core.persistence.api.entity.auth.OIDCRP;
+import org.apache.syncope.core.persistence.api.entity.auth.SAML2SP;
+
+public class JPAClientAppUtils implements ClientAppUtils {
+
+    private final ClientAppType type;
+
+    protected JPAClientAppUtils(final ClientAppType type) {
+        this.type = type;
+    }
+
+    @Override
+    public ClientAppType getType() {
+        return type;
+    }
+
+    @Override
+    public Class<? extends ClientApp> clientAppClass() {
+        switch (type) {
+            case OIDCRP:
+                return OIDCRP.class;
+
+            case SAML2SP:
+            default:
+                return SAML2SP.class;
+        }
+    }
+}
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/auth/JPAClientAppUtilsFactory.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/auth/JPAClientAppUtilsFactory.java
new file mode 100644
index 0000000..610cc63
--- /dev/null
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/auth/JPAClientAppUtilsFactory.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.persistence.jpa.entity.auth;
+
+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.ClientAppType;
+import org.apache.syncope.core.persistence.api.entity.auth.ClientApp;
+import org.apache.syncope.core.persistence.api.entity.auth.ClientAppUtils;
+import org.apache.syncope.core.persistence.api.entity.auth.ClientAppUtilsFactory;
+import org.apache.syncope.core.persistence.api.entity.auth.OIDCRP;
+import org.apache.syncope.core.persistence.api.entity.auth.SAML2SP;
+import org.springframework.stereotype.Component;
+
+@Component
+public class JPAClientAppUtilsFactory implements ClientAppUtilsFactory {
+
+    @Override
+    public ClientAppUtils getInstance(final ClientAppType type) {
+        return new JPAClientAppUtils(type);
+    }
+
+    @Override
+    public ClientAppUtils getInstance(final ClientApp clientApp) {
+        ClientAppType type;
+        if (clientApp instanceof SAML2SP) {
+            type = ClientAppType.SAML2SP;
+        } else if (clientApp instanceof OIDCRP) {
+            type = ClientAppType.OIDCRP;
+        } else {
+            throw new IllegalArgumentException("Invalid client app: " + clientApp);
+        }
+
+        return getInstance(type);
+    }
+
+    @Override
+    public ClientAppUtils getInstance(final Class<? extends ClientAppTO> clientAppClass) {
+        ClientAppType type;
+        if (clientAppClass == SAML2SPTO.class) {
+            type = ClientAppType.SAML2SP;
+        } else if (clientAppClass == OIDCRPTO.class) {
+            type = ClientAppType.OIDCRP;
+        } else {
+            throw new IllegalArgumentException("Invalid ClientAppTO app: " + clientAppClass.getName());
+        }
+
+        return getInstance(type);
+    }
+
+    @Override
+    public ClientAppUtils getInstance(final ClientAppTO clientAppTO) {
+        return getInstance(clientAppTO.getClass());
+    }
+}
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/auth/JPAOIDCRP.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/auth/JPAOIDCRP.java
new file mode 100644
index 0000000..a4f9a7b
--- /dev/null
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/auth/JPAOIDCRP.java
@@ -0,0 +1,143 @@
+/*
+ * 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.CollectionTable;
+import javax.persistence.Column;
+import javax.persistence.ElementCollection;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.JoinColumn;
+import javax.persistence.Table;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import org.apache.syncope.common.lib.types.OIDCSubjectType;
+import org.apache.syncope.core.persistence.api.entity.auth.OIDCRP;
+
+@Entity
+@Table(name = JPAOIDCRP.TABLE)
+public class JPAOIDCRP extends AbstractClientApp implements OIDCRP {
+
+    private static final long serialVersionUID = 7422422526695279794L;
+
+    public static final String TABLE = "OIDCRP";
+
+    @Column(unique = true, nullable = false)
+    private String clientId;
+
+    @Column
+    private String clientSecret;
+
+    @Column
+    private boolean signIdToken;
+
+    @Column
+    private String jwks;
+
+    @Column
+    private OIDCSubjectType subjectType;
+
+    @ElementCollection(fetch = FetchType.EAGER)
+    @Column
+    @CollectionTable(name = "OIDCRP_RedirectUris",
+            joinColumns =
+            @JoinColumn(name = "client_id", referencedColumnName = "id"))
+    private List<String> redirectUris = new ArrayList<>();
+
+    @ElementCollection(fetch = FetchType.EAGER)
+    @Column
+    @CollectionTable(name = "OIDCRP_SupportedGrantTypes",
+            joinColumns =
+            @JoinColumn(name = "client_id", referencedColumnName = "id"))
+    private Set<String> supportedGrantTypes = new HashSet<>();
+
+    @ElementCollection(fetch = FetchType.EAGER)
+    @Column(name = "supportedResponseType")
+    @CollectionTable(name = "OIDCRP_SupportedResponseTypes",
+            joinColumns =
+            @JoinColumn(name = "client_id", referencedColumnName = "id"))
+    private Set<String> supportedResponseTypes = new HashSet<>();
+
+    @Override
+    public List<String> getRedirectUris() {
+        return redirectUris;
+    }
+
+    @Override
+    public String getClientId() {
+        return clientId;
+    }
+
+    @Override
+    public void setClientId(final String clientId) {
+        this.clientId = clientId;
+    }
+
+    @Override
+    public String getClientSecret() {
+        return clientSecret;
+    }
+
+    @Override
+    public void setClientSecret(final String clientSecret) {
+        this.clientSecret = clientSecret;
+    }
+
+    @Override
+    public boolean isSignIdToken() {
+        return signIdToken;
+    }
+
+    @Override
+    public void setSignIdToken(final boolean signIdToken) {
+        this.signIdToken = signIdToken;
+    }
+
+    @Override
+    public String getJwks() {
+        return jwks;
+    }
+
+    @Override
+    public void setJwks(final String jwks) {
+        this.jwks = jwks;
+    }
+
+    @Override
+    public OIDCSubjectType getSubjectType() {
+        return subjectType;
+    }
+
+    @Override
+    public void setSubjectType(final OIDCSubjectType subjectType) {
+        this.subjectType = subjectType;
+    }
+
+    @Override
+    public Set<String> getSupportedGrantTypes() {
+        return supportedGrantTypes;
+    }
+
+    @Override
+    public Set<String> getSupportedResponseTypes() {
+        return supportedResponseTypes;
+    }
+}
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/auth/JPASAML2SP.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/auth/JPASAML2SP.java
new file mode 100644
index 0000000..a34f0e5
--- /dev/null
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/auth/JPASAML2SP.java
@@ -0,0 +1,204 @@
+/*
+ * 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.Table;
+import org.apache.syncope.common.lib.types.SAML2SPNameId;
+import org.apache.syncope.core.persistence.api.entity.auth.SAML2SP;
+
+@Entity
+@Table(name = JPASAML2SP.TABLE)
+public class JPASAML2SP extends AbstractClientApp implements SAML2SP {
+
+    public static final String TABLE = "SAML2SP";
+
+    private static final long serialVersionUID = 6422422526695279794L;
+
+    @Column(unique = true, nullable = false)
+    private String entityId;
+
+    @Column(nullable = false)
+    private String metadataLocation;
+
+    @Column
+    private String metadataSignatureLocation;
+
+    @Column
+    private boolean signAssertions;
+
+    @Column
+    private boolean signResponses;
+
+    @Column
+    private boolean encryptionOptional;
+
+    @Column
+    private boolean encryptAssertions;
+
+    @Column(name = "reqAuthnContextClass")
+    private String requiredAuthenticationContextClass;
+
+    @Column
+    private SAML2SPNameId requiredNameIdFormat;
+
+    @Column
+    private Integer skewAllowance;
+
+    @Column
+    private String nameIdQualifier;
+
+    @Column
+    private String assertionAudiences;
+
+    @Column(name = "spNameIdQualifier")
+    private String serviceProviderNameIdQualifier;
+
+    @Override
+    public String getEntityId() {
+        return entityId;
+    }
+
+    @Override
+    public void setEntityId(final String entityId) {
+        this.entityId = entityId;
+    }
+
+    @Override
+    public String getMetadataLocation() {
+        return metadataLocation;
+    }
+
+    @Override
+    public void setMetadataLocation(final String metadataLocation) {
+        this.metadataLocation = metadataLocation;
+    }
+
+    @Override
+    public String getMetadataSignatureLocation() {
+        return metadataSignatureLocation;
+    }
+
+    @Override
+    public void setMetadataSignatureLocation(final String metadataSignatureLocation) {
+        this.metadataSignatureLocation = metadataSignatureLocation;
+    }
+
+    @Override
+    public boolean isSignAssertions() {
+        return signAssertions;
+    }
+
+    @Override
+    public void setSignAssertions(final boolean signAssertions) {
+        this.signAssertions = signAssertions;
+    }
+
+    @Override
+    public boolean isSignResponses() {
+        return signResponses;
+    }
+
+    @Override
+    public void setSignResponses(final boolean signResponses) {
+        this.signResponses = signResponses;
+    }
+
+    @Override
+    public boolean isEncryptionOptional() {
+        return encryptionOptional;
+    }
+
+    @Override
+    public void setEncryptionOptional(final boolean encryptionOptional) {
+        this.encryptionOptional = encryptionOptional;
+    }
+
+    @Override
+    public boolean isEncryptAssertions() {
+        return encryptAssertions;
+    }
+
+    @Override
+    public void setEncryptAssertions(final boolean encryptAssertions) {
+        this.encryptAssertions = encryptAssertions;
+    }
+
+    @Override
+    public String getRequiredAuthenticationContextClass() {
+        return requiredAuthenticationContextClass;
+    }
+
+    @Override
+    public void setRequiredAuthenticationContextClass(final String requiredAuthenticationContextClass) {
+        this.requiredAuthenticationContextClass = requiredAuthenticationContextClass;
+    }
+
+    @Override
+    public SAML2SPNameId getRequiredNameIdFormat() {
+        return requiredNameIdFormat;
+    }
+
+    @Override
+    public void setRequiredNameIdFormat(final SAML2SPNameId requiredNameIdFormat) {
+        this.requiredNameIdFormat = requiredNameIdFormat;
+    }
+
+    @Override
+    public Integer getSkewAllowance() {
+        return skewAllowance;
+    }
+
+    @Override
+    public void setSkewAllowance(final Integer skewAllowance) {
+        this.skewAllowance = skewAllowance;
+    }
+
+    @Override
+    public String getNameIdQualifier() {
+        return nameIdQualifier;
+    }
+
+    @Override
+    public void setNameIdQualifier(final String nameIdQualifier) {
+        this.nameIdQualifier = nameIdQualifier;
+    }
+
+    @Override
+    public String getAssertionAudiences() {
+        return assertionAudiences;
+    }
+
+    @Override
+    public void setAssertionAudiences(final String assertionAudiences) {
+        this.assertionAudiences = assertionAudiences;
+    }
+
+    @Override
+    public String getServiceProviderNameIdQualifier() {
+        return serviceProviderNameIdQualifier;
+    }
+
+    @Override
+    public void setServiceProviderNameIdQualifier(final String serviceProviderNameIdQualifier) {
+        this.serviceProviderNameIdQualifier = serviceProviderNameIdQualifier;
+    }
+
+}
diff --git a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AbstractClientAppTest.java b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AbstractClientAppTest.java
new file mode 100644
index 0000000..d2eea8f
--- /dev/null
+++ b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AbstractClientAppTest.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.core.persistence.jpa.inner;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.syncope.common.lib.policy.DefaultAccessPolicyConf;
+import org.apache.syncope.common.lib.policy.AllowedAttrReleasePolicyConf;
+import org.apache.syncope.common.lib.policy.DefaultAuthPolicyConf;
+import org.apache.syncope.common.lib.types.AMImplementationType;
+import org.apache.syncope.common.lib.types.ImplementationEngine;
+import org.apache.syncope.core.persistence.api.dao.ImplementationDAO;
+import org.apache.syncope.core.persistence.api.entity.Implementation;
+import org.apache.syncope.core.persistence.api.entity.policy.AccessPolicy;
+import org.apache.syncope.core.persistence.api.entity.policy.AttrReleasePolicy;
+import org.apache.syncope.core.persistence.api.entity.policy.AuthPolicy;
+import org.apache.syncope.core.persistence.api.dao.PolicyDAO;
+import org.apache.syncope.core.persistence.jpa.AbstractTest;
+import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
+import org.springframework.beans.factory.annotation.Autowired;
+
+public class AbstractClientAppTest extends AbstractTest {
+
+    @Autowired
+    protected PolicyDAO policyDAO;
+
+    @Autowired
+    protected ImplementationDAO implementationDAO;
+
+    protected AttrReleasePolicy buildAndSaveAttrRelPolicy() {
+        AttrReleasePolicy attrRelPolicy = entityFactory.newEntity(AttrReleasePolicy.class);
+        attrRelPolicy.setName("AttrRelPolicyTest");
+        attrRelPolicy.setDescription("This is a sample access policy");
+
+        AllowedAttrReleasePolicyConf conf = new AllowedAttrReleasePolicyConf();
+        conf.setName("Example Attr Rel Policy for an application");
+        conf.getAllowedAttributes().addAll(List.of("cn", "givenName"));
+
+        Implementation type = entityFactory.newEntity(Implementation.class);
+        type.setKey("AttrRelPolicyTest");
+        type.setEngine(ImplementationEngine.JAVA);
+        type.setType(AMImplementationType.ATTR_RELEASE_POLICY_CONFIGURATIONS);
+        type.setBody(POJOHelper.serialize(conf));
+        type = implementationDAO.save(type);
+        attrRelPolicy.setConfiguration(type);
+        return policyDAO.save(attrRelPolicy);
+
+    }
+
+    protected AccessPolicy buildAndSaveAccessPolicy() {
+        AccessPolicy accessPolicy = entityFactory.newEntity(AccessPolicy.class);
+        accessPolicy.setName("AccessPolicyTest");
+        accessPolicy.setDescription("This is a sample access policy");
+
+        DefaultAccessPolicyConf conf = new DefaultAccessPolicyConf();
+        conf.setEnabled(true);
+        conf.setName("Example Access Policy for an application");
+        conf.getRequiredAttributes().putAll(Map.of("attribute1", Set.of("value1", "value2")));
+        conf.setSsoEnabled(false);
+
+        Implementation type = entityFactory.newEntity(Implementation.class);
+        type.setKey("AccessPolicyConfKey");
+        type.setEngine(ImplementationEngine.JAVA);
+        type.setType(AMImplementationType.ACCESS_POLICY_CONFIGURATIONS);
+        type.setBody(POJOHelper.serialize(conf));
+        type = implementationDAO.save(type);
+
+        accessPolicy.setConfiguration(type);
+        return policyDAO.save(accessPolicy);
+
+    }
+
+    protected AuthPolicy buildAndSaveAuthPolicy() {
+        AuthPolicy authPolicy = entityFactory.newEntity(AuthPolicy.class);
+        authPolicy.setName("AuthPolicyTest");
+        authPolicy.setDescription("This is a sample authentication policy");
+
+        DefaultAuthPolicyConf conf = new DefaultAuthPolicyConf();
+        conf.getAuthModules().addAll(List.of("LdapAuthentication1", "DatabaseAuthentication2"));
+
+        Implementation type = entityFactory.newEntity(Implementation.class);
+        type.setKey("AuthPolicyConfKey");
+        type.setEngine(ImplementationEngine.JAVA);
+        type.setType(AMImplementationType.AUTH_POLICY_CONFIGURATIONS);
+        type.setBody(POJOHelper.serialize(conf));
+        type = implementationDAO.save(type);
+
+        authPolicy.setConfiguration(type);
+        return policyDAO.save(authPolicy);
+    }
+
+}
diff --git a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/OIDCRPTest.java b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/OIDCRPTest.java
new file mode 100644
index 0000000..d79677a
--- /dev/null
+++ b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/OIDCRPTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.common.lib.types.OIDCSubjectType;
+import org.apache.syncope.core.persistence.api.dao.auth.OIDCRPDAO;
+import org.apache.syncope.core.persistence.api.entity.auth.OIDCRP;
+import org.apache.syncope.core.persistence.api.entity.policy.AccessPolicy;
+import org.apache.syncope.core.persistence.api.entity.policy.AuthPolicy;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
+
+@Transactional("Master")
+public class OIDCRPTest extends AbstractClientAppTest {
+
+    @Autowired
+    private OIDCRPDAO oidcrpDAO;
+
+    @Test
+    public void find() {
+        int beforeCount = oidcrpDAO.findAll().size();
+
+        OIDCRP rp = entityFactory.newEntity(OIDCRP.class);
+        rp.setName("OIDC");
+        rp.setClientAppId(UUID.randomUUID().getMostSignificantBits() & Long.MAX_VALUE);
+        rp.setDescription("This is a sample OIDC RP");
+        rp.setClientId("clientid");
+        rp.setClientSecret("secret");
+        rp.setSubjectType(OIDCSubjectType.PUBLIC);
+        rp.getSupportedGrantTypes().add("something");
+        rp.getSupportedResponseTypes().add("something");
+
+        AccessPolicy accessPolicy = buildAndSaveAccessPolicy();
+        rp.setAccessPolicy(accessPolicy);
+
+        AuthPolicy authPolicy = buildAndSaveAuthPolicy();
+        rp.setAuthPolicy(authPolicy);
+
+        oidcrpDAO.save(rp);
+
+        assertNotNull(rp);
+        assertNotNull(rp.getKey());
+
+        int afterCount = oidcrpDAO.findAll().size();
+        assertEquals(afterCount, beforeCount + 1);
+
+        rp = oidcrpDAO.findByClientId("clientid");
+        assertNotNull(rp);
+        assertNotNull(rp.getAuthPolicy());
+
+        rp = oidcrpDAO.findByName("OIDC");
+        assertNotNull(rp);
+        
+        rp = oidcrpDAO.findByClientAppId(rp.getClientAppId());
+        assertNotNull(rp);
+
+        oidcrpDAO.deleteByClientId("clientid");
+        assertNull(oidcrpDAO.findByName("OIDC"));
+    }
+}
diff --git a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/SAML2SPTest.java b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/SAML2SPTest.java
new file mode 100644
index 0000000..a5e50c7
--- /dev/null
+++ b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/SAML2SPTest.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.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.entity.policy.AccessPolicy;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
+import org.apache.syncope.common.lib.types.SAML2SPNameId;
+import org.apache.syncope.core.persistence.api.entity.policy.AuthPolicy;
+import org.apache.syncope.core.persistence.api.dao.auth.SAML2SPDAO;
+import org.apache.syncope.core.persistence.api.entity.auth.SAML2SP;
+
+@Transactional("Master")
+public class SAML2SPTest extends AbstractClientAppTest {
+
+    @Autowired
+    private SAML2SPDAO saml2spDAO;
+
+    @Test
+    public void find() {
+        int beforeCount = saml2spDAO.findAll().size();
+        SAML2SP sp = entityFactory.newEntity(SAML2SP.class);
+        sp.setName("SAML2");
+        sp.setClientAppId(UUID.randomUUID().getMostSignificantBits() & Long.MAX_VALUE);
+        sp.setDescription("This is a sample SAML2 SP");
+        sp.setEntityId("urn:example:saml2:sp");
+        sp.setMetadataLocation("https://example.org/metadata.xml");
+        sp.setRequiredNameIdFormat(SAML2SPNameId.EMAIL_ADDRESS);
+        sp.setEncryptionOptional(true);
+        sp.setEncryptAssertions(true);
+
+        AccessPolicy accessPolicy = buildAndSaveAccessPolicy();
+        sp.setAccessPolicy(accessPolicy);
+
+        AuthPolicy authnPolicy = buildAndSaveAuthPolicy();
+        sp.setAuthPolicy(authnPolicy);
+
+        saml2spDAO.save(sp);
+
+        assertNotNull(sp);
+        assertNotNull(sp.getKey());
+
+        int afterCount = saml2spDAO.findAll().size();
+        assertEquals(afterCount, beforeCount + 1);
+
+        sp = saml2spDAO.findByEntityId(sp.getEntityId());
+        assertNotNull(sp);
+
+        sp = saml2spDAO.findByName(sp.getName());
+        assertNotNull(sp);
+
+        sp = saml2spDAO.findByClientAppId(sp.getClientAppId());
+        assertNotNull(sp);
+
+        saml2spDAO.deleteByEntityId(sp.getEntityId());
+        assertNull(saml2spDAO.findByName(sp.getName()));
+    }
+}
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/ClientAppDataBinder.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/ClientAppDataBinder.java
new file mode 100644
index 0000000..0590122
--- /dev/null
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/ClientAppDataBinder.java
@@ -0,0 +1,31 @@
+/*
+ * 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.client.ClientAppTO;
+import org.apache.syncope.core.persistence.api.entity.auth.ClientApp;
+
+public interface ClientAppDataBinder {
+
+    <T extends ClientApp> T create(ClientAppTO clientAppTO);
+
+    <T extends ClientApp> void update(T clientApp, ClientAppTO clientAppTO);
+
+    <T extends ClientAppTO> T getClientAppTO(ClientApp clientApp);
+}
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/ClientAppDataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/ClientAppDataBinderImpl.java
new file mode 100644
index 0000000..67db455
--- /dev/null
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/ClientAppDataBinderImpl.java
@@ -0,0 +1,268 @@
+/*
+ * 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.SyncopeClientException;
+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.ClientExceptionType;
+import org.apache.syncope.core.persistence.api.dao.PolicyDAO;
+import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.api.entity.auth.ClientApp;
+import org.apache.syncope.core.persistence.api.entity.auth.SAML2SP;
+import org.apache.syncope.core.persistence.api.entity.policy.AccessPolicy;
+import org.apache.syncope.core.persistence.api.entity.policy.AttrReleasePolicy;
+import org.apache.syncope.core.persistence.api.entity.policy.AuthPolicy;
+import org.apache.syncope.core.persistence.api.entity.policy.Policy;
+import org.apache.syncope.core.provisioning.api.data.ClientAppDataBinder;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.apache.syncope.core.persistence.api.entity.auth.OIDCRP;
+
+@Component
+public class ClientAppDataBinderImpl implements ClientAppDataBinder {
+
+    @Autowired
+    private PolicyDAO policyDAO;
+
+    @Autowired
+    private EntityFactory entityFactory;
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T extends ClientApp> T create(final ClientAppTO clientAppTO) {
+        if (clientAppTO instanceof SAML2SPTO) {
+            return (T) doCreate((SAML2SPTO) clientAppTO);
+        } else if (clientAppTO instanceof OIDCRPTO) {
+            return (T) doCreate((OIDCRPTO) clientAppTO);
+        } else {
+            throw new IllegalArgumentException("Unsupported client app: " + clientAppTO.getClass().getName());
+        }
+    }
+
+    @Override
+    public <T extends ClientApp> void update(final T clientApp, final ClientAppTO clientAppTO) {
+        if (clientAppTO instanceof SAML2SPTO) {
+            doUpdate((SAML2SP) clientApp, (SAML2SPTO) clientAppTO);
+        } else if (clientAppTO instanceof OIDCRPTO) {
+            doUpdate((OIDCRP) clientApp, (OIDCRPTO) clientAppTO);
+        } else {
+            throw new IllegalArgumentException("Unsupported client app: " + clientAppTO.getClass().getName());
+        }
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T extends ClientAppTO> T getClientAppTO(final ClientApp clientApp) {
+        if (clientApp instanceof SAML2SP) {
+            return (T) getClientAppTO((SAML2SP) clientApp);
+        } else if (clientApp instanceof OIDCRP) {
+            return (T) getClientAppTO((OIDCRP) clientApp);
+        } else {
+            throw new IllegalArgumentException("Unsupported client app: " + clientApp.getClass().getName());
+        }
+    }
+
+    private SAML2SP doCreate(final SAML2SPTO clientAppTO) {
+        SAML2SP saml2sp = entityFactory.newEntity(SAML2SP.class);
+        update(saml2sp, clientAppTO);
+        return saml2sp;
+    }
+
+    private void doUpdate(final SAML2SP clientApp, final SAML2SPTO clientAppTO) {
+        clientApp.setDescription(clientAppTO.getDescription());
+        clientApp.setName(clientAppTO.getName());
+        clientApp.setClientAppId(clientAppTO.getClientAppId());
+        clientApp.setEntityId(clientAppTO.getEntityId());
+        clientApp.setMetadataLocation(clientAppTO.getMetadataLocation());
+        clientApp.setMetadataSignatureLocation(clientAppTO.getMetadataLocation());
+        clientApp.setSignAssertions(clientAppTO.isSignAssertions());
+        clientApp.setSignResponses(clientAppTO.isSignResponses());
+        clientApp.setEncryptionOptional(clientAppTO.isEncryptionOptional());
+        clientApp.setEncryptAssertions(clientAppTO.isEncryptAssertions());
+        clientApp.setRequiredAuthenticationContextClass(clientAppTO.getRequiredAuthenticationContextClass());
+        clientApp.setRequiredNameIdFormat(clientAppTO.getRequiredNameIdFormat());
+        clientApp.setSkewAllowance(clientAppTO.getSkewAllowance());
+        clientApp.setNameIdQualifier(clientAppTO.getNameIdQualifier());
+        clientApp.setAssertionAudiences(clientAppTO.getAssertionAudiences());
+        clientApp.setServiceProviderNameIdQualifier(clientAppTO.getServiceProviderNameIdQualifier());
+
+        if (clientAppTO.getAuthPolicy() == null) {
+            clientApp.setAuthPolicy(null);
+        } else {
+            Policy policy = policyDAO.find(clientAppTO.getAuthPolicy());
+            if (policy instanceof AuthPolicy) {
+                clientApp.setAuthPolicy((AuthPolicy) policy);
+            } else {
+                SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidPolicy);
+                sce.getElements().add("Expected " + AuthPolicy.class.getSimpleName()
+                        + ", found " + policy.getClass().getSimpleName());
+                throw sce;
+            }
+        }
+
+        if (clientAppTO.getAccessPolicy() == null) {
+            clientApp.setAccessPolicy(null);
+        } else {
+            Policy policy = policyDAO.find(clientAppTO.getAccessPolicy());
+            if (policy instanceof AccessPolicy) {
+                clientApp.setAccessPolicy((AccessPolicy) policy);
+            } else {
+                SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidPolicy);
+                sce.getElements().add("Expected " + AccessPolicy.class.getSimpleName()
+                        + ", found " + policy.getClass().getSimpleName());
+                throw sce;
+            }
+        }
+
+        if (clientAppTO.getAttrReleasePolicy() == null) {
+            clientApp.setAttrReleasePolicy(null);
+        } else {
+            Policy policy = policyDAO.find(clientAppTO.getAttrReleasePolicy());
+            if (policy instanceof AttrReleasePolicy) {
+                clientApp.setAttrReleasePolicy((AttrReleasePolicy) policy);
+            } else {
+                SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidPolicy);
+                sce.getElements().add("Expected " + AttrReleasePolicy.class.getSimpleName()
+                        + ", found " + policy.getClass().getSimpleName());
+                throw sce;
+            }
+        }
+    }
+
+    private SAML2SPTO getClientAppTO(final SAML2SP clientApp) {
+        SAML2SPTO clientAppTO = new SAML2SPTO();
+
+        clientAppTO.setName(clientApp.getName());
+        clientAppTO.setKey(clientApp.getKey());
+        clientAppTO.setDescription(clientApp.getDescription());
+        clientAppTO.setClientAppId(clientApp.getClientAppId());
+        clientAppTO.setEntityId(clientApp.getEntityId());
+        clientAppTO.setMetadataLocation(clientApp.getMetadataLocation());
+        clientAppTO.setMetadataSignatureLocation(clientApp.getMetadataLocation());
+        clientAppTO.setSignAssertions(clientApp.isSignAssertions());
+        clientAppTO.setSignResponses(clientApp.isSignResponses());
+        clientAppTO.setEncryptionOptional(clientApp.isEncryptionOptional());
+        clientAppTO.setEncryptAssertions(clientApp.isEncryptAssertions());
+        clientAppTO.setRequiredAuthenticationContextClass(clientApp.getRequiredAuthenticationContextClass());
+        clientAppTO.setRequiredNameIdFormat(clientApp.getRequiredNameIdFormat());
+        clientAppTO.setSkewAllowance(clientApp.getSkewAllowance());
+        clientAppTO.setNameIdQualifier(clientApp.getNameIdQualifier());
+        clientAppTO.setAssertionAudiences(clientApp.getAssertionAudiences());
+        clientAppTO.setServiceProviderNameIdQualifier(clientApp.getServiceProviderNameIdQualifier());
+
+        clientAppTO.setAuthPolicy(clientApp.getAuthPolicy() == null
+                ? null : clientApp.getAuthPolicy().getKey());
+        clientAppTO.setAccessPolicy(clientApp.getAccessPolicy() == null
+                ? null : clientApp.getAccessPolicy().getKey());
+        clientAppTO.setAttrReleasePolicy(clientApp.getAttrReleasePolicy() == null
+                ? null : clientApp.getAttrReleasePolicy().getKey());
+
+        return clientAppTO;
+    }
+
+    private OIDCRP doCreate(final OIDCRPTO clientAppTO) {
+        OIDCRP oidcrp = entityFactory.newEntity(OIDCRP.class);
+        update(oidcrp, clientAppTO);
+        return oidcrp;
+    }
+
+    private void doUpdate(final OIDCRP clientApp, final OIDCRPTO clientAppTO) {
+        clientApp.setName(clientAppTO.getName());
+        clientApp.setClientAppId(clientAppTO.getClientAppId());
+        clientApp.setDescription(clientAppTO.getDescription());
+        clientApp.setClientSecret(clientAppTO.getClientSecret());
+        clientApp.setClientId(clientAppTO.getClientId());
+        clientApp.setSignIdToken(clientAppTO.isSignIdToken());
+        clientApp.setJwks(clientAppTO.getJwks());
+        clientApp.setSubjectType(clientAppTO.getSubjectType());
+        clientApp.getRedirectUris().addAll(clientAppTO.getRedirectUris());
+        clientApp.getSupportedGrantTypes().addAll(clientAppTO.getSupportedGrantTypes());
+        clientApp.getSupportedResponseTypes().addAll(clientAppTO.getSupportedResponseTypes());
+
+        if (clientAppTO.getAuthPolicy() == null) {
+            clientApp.setAuthPolicy(null);
+        } else {
+            Policy policy = policyDAO.find(clientAppTO.getAuthPolicy());
+            if (policy instanceof AuthPolicy) {
+                clientApp.setAuthPolicy((AuthPolicy) policy);
+            } else {
+                SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidPolicy);
+                sce.getElements().add("Expected " + AuthPolicy.class.getSimpleName()
+                        + ", found " + policy.getClass().getSimpleName());
+                throw sce;
+            }
+        }
+
+        if (clientAppTO.getAccessPolicy() == null) {
+            clientApp.setAccessPolicy(null);
+        } else {
+            Policy policy = policyDAO.find(clientAppTO.getAccessPolicy());
+            if (policy instanceof AccessPolicy) {
+                clientApp.setAccessPolicy((AccessPolicy) policy);
+            } else {
+                SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidPolicy);
+                sce.getElements().add("Expected " + AccessPolicy.class.getSimpleName()
+                        + ", found " + policy.getClass().getSimpleName());
+                throw sce;
+            }
+        }
+
+        if (clientAppTO.getAttrReleasePolicy() == null) {
+            clientApp.setAttrReleasePolicy(null);
+        } else {
+            Policy policy = policyDAO.find(clientAppTO.getAttrReleasePolicy());
+            if (policy instanceof AttrReleasePolicy) {
+                clientApp.setAttrReleasePolicy((AttrReleasePolicy) policy);
+            } else {
+                SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidPolicy);
+                sce.getElements().add("Expected " + AttrReleasePolicy.class.getSimpleName()
+                        + ", found " + policy.getClass().getSimpleName());
+                throw sce;
+            }
+        }
+    }
+
+    private OIDCRPTO getClientAppTO(final OIDCRP clientApp) {
+        OIDCRPTO clientAppTO = new OIDCRPTO();
+
+        clientAppTO.setName(clientApp.getName());
+        clientAppTO.setKey(clientApp.getKey());
+        clientAppTO.setDescription(clientApp.getDescription());
+        clientAppTO.setClientAppId(clientApp.getClientAppId());
+        clientAppTO.setClientId(clientApp.getClientId());
+        clientAppTO.setClientSecret(clientApp.getClientSecret());
+        clientAppTO.setSignIdToken(clientApp.isSignIdToken());
+        clientAppTO.setJwks(clientApp.getJwks());
+        clientAppTO.setSubjectType(clientApp.getSubjectType());
+        clientAppTO.getRedirectUris().addAll(clientApp.getRedirectUris());
+        clientAppTO.getSupportedGrantTypes().addAll(clientApp.getSupportedGrantTypes());
+        clientAppTO.getSupportedResponseTypes().addAll(clientApp.getSupportedResponseTypes());
+
+        clientAppTO.setAuthPolicy(clientApp.getAuthPolicy() == null
+                ? null : clientApp.getAuthPolicy().getKey());
+        clientAppTO.setAccessPolicy(clientApp.getAccessPolicy() == null
+                ? null : clientApp.getAccessPolicy().getKey());
+        clientAppTO.setAttrReleasePolicy(clientApp.getAttrReleasePolicy() == null
+                ? null : clientApp.getAttrReleasePolicy().getKey());
+
+        return clientAppTO;
+    }
+}
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/ClientAppITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/ClientAppITCase.java
new file mode 100644
index 0000000..3ee1586
--- /dev/null
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/ClientAppITCase.java
@@ -0,0 +1,155 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.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.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.to.AccessPolicyTO;
+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.ClientAppType;
+import org.apache.syncope.common.lib.types.PolicyType;
+import org.apache.syncope.fit.AbstractITCase;
+import org.junit.jupiter.api.Test;
+
+public class ClientAppITCase extends AbstractITCase {
+
+    @Test
+    public void createSAML2SP() {
+        createClientApp(ClientAppType.SAML2SP, buildSAML2SP());
+    }
+
+    @Test
+    public void readSAML2SP() {
+        SAML2SPTO samlSpTO = buildSAML2SP();
+        samlSpTO = createClientApp(ClientAppType.SAML2SP, samlSpTO);
+
+        SAML2SPTO found = clientAppService.read(ClientAppType.SAML2SP, samlSpTO.getKey());
+        assertNotNull(found);
+        assertFalse(StringUtils.isBlank(found.getEntityId()));
+        assertFalse(StringUtils.isBlank(found.getMetadataLocation()));
+        assertTrue(found.isEncryptAssertions());
+        assertTrue(found.isEncryptionOptional());
+        assertNotNull(found.getRequiredNameIdFormat());
+        assertNotNull(found.getAccessPolicy());
+        assertNotNull(found.getAuthPolicy());
+    }
+
+    @Test
+    public void updateSAML2SP() {
+        SAML2SPTO samlSpTO = buildSAML2SP();
+        samlSpTO = createClientApp(ClientAppType.SAML2SP, samlSpTO);
+
+        AccessPolicyTO accessPolicyTO = new AccessPolicyTO();
+        accessPolicyTO.setKey("NewAccessPolicyTest_" + getUUIDString());
+        accessPolicyTO.setDescription("New Access policy");
+        accessPolicyTO = createPolicy(PolicyType.ACCESS, accessPolicyTO);
+        assertNotNull(accessPolicyTO);
+
+        samlSpTO.setEntityId("newEntityId");
+        samlSpTO.setAccessPolicy(accessPolicyTO.getKey());
+
+        clientAppService.update(ClientAppType.SAML2SP, samlSpTO);
+        SAML2SPTO updated = clientAppService.read(ClientAppType.SAML2SP, samlSpTO.getKey());
+
+        assertNotNull(updated);
+        assertEquals("newEntityId", updated.getEntityId());
+        assertNotNull(updated.getAccessPolicy());
+    }
+
+    @Test
+    public void deleteSAML2SP() {
+        SAML2SPTO samlSpTO = buildSAML2SP();
+        samlSpTO = createClientApp(ClientAppType.SAML2SP, samlSpTO);
+
+        clientAppService.delete(ClientAppType.SAML2SP, samlSpTO.getKey());
+
+        try {
+            clientAppService.read(ClientAppType.SAML2SP, samlSpTO.getKey());
+            fail("This should not happen");
+        } catch (SyncopeClientException e) {
+            assertNotNull(e);
+        }
+    }
+
+    @Test
+    public void createOIDCRP() {
+        createClientApp(ClientAppType.OIDCRP, buildOIDCRP());
+    }
+
+    @Test
+    public void readOIDCRP() {
+        OIDCRPTO oidcrpTO = buildOIDCRP();
+        oidcrpTO = createClientApp(ClientAppType.OIDCRP, oidcrpTO);
+
+        OIDCRPTO found = clientAppService.read(ClientAppType.OIDCRP, oidcrpTO.getKey());
+        assertNotNull(found);
+        assertFalse(StringUtils.isBlank(found.getClientId()));
+        assertFalse(StringUtils.isBlank(found.getClientSecret()));
+        assertNotNull(found.getSubjectType());
+        assertFalse(found.getSupportedGrantTypes().isEmpty());
+        assertFalse(found.getSupportedResponseTypes().isEmpty());
+        assertNotNull(found.getAccessPolicy());
+        assertNotNull(found.getAuthPolicy());
+    }
+
+    @Test
+    public void updateOIDCRP() {
+        OIDCRPTO oidcrpTO = buildOIDCRP();
+        oidcrpTO = createClientApp(ClientAppType.OIDCRP, oidcrpTO);
+
+        AccessPolicyTO accessPolicyTO = new AccessPolicyTO();
+        accessPolicyTO.setKey("NewAccessPolicyTest_" + getUUIDString());
+        accessPolicyTO.setDescription("New Access policy");
+        accessPolicyTO = createPolicy(PolicyType.ACCESS, accessPolicyTO);
+        assertNotNull(accessPolicyTO);
+
+        oidcrpTO.setClientId("newClientId");
+        oidcrpTO.setAccessPolicy(accessPolicyTO.getKey());
+
+        clientAppService.update(ClientAppType.OIDCRP, oidcrpTO);
+        OIDCRPTO updated = clientAppService.read(ClientAppType.OIDCRP, oidcrpTO.getKey());
+
+        assertNotNull(updated);
+        assertEquals("newClientId", updated.getClientId());
+        assertNotNull(updated.getAccessPolicy());
+    }
+
+    @Test
+    public void delete() {
+        OIDCRPTO oidcrpTO = buildOIDCRP();
+        oidcrpTO = createClientApp(ClientAppType.OIDCRP, oidcrpTO);
+
+        clientAppService.delete(ClientAppType.OIDCRP, oidcrpTO.getKey());
+
+        try {
+            clientAppService.read(ClientAppType.OIDCRP, oidcrpTO.getKey());
+            fail("This should not happen");
+        } catch (SyncopeClientException e) {
+            assertNotNull(e);
+        }
+    }
+
+}