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 2018/05/03 14:58:13 UTC

[6/8] syncope git commit: [SYNCOPE-1270] implementation for OpenID Connect for Admin Console and Enduser - This closes #74

http://git-wip-us.apache.org/repos/asf/syncope/blob/797fd1cb/ext/oidcclient/logic/pom.xml
----------------------------------------------------------------------
diff --git a/ext/oidcclient/logic/pom.xml b/ext/oidcclient/logic/pom.xml
new file mode 100644
index 0000000..6701bc3
--- /dev/null
+++ b/ext/oidcclient/logic/pom.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.syncope.ext</groupId>
+    <artifactId>syncope-ext-oidcclient</artifactId>
+    <version>2.1.0-SNAPSHOT</version>
+  </parent>
+
+  <name>Apache Syncope Ext: OIDC Client Logic</name>
+  <description>Apache Syncope Ext: OIDC Client Logic</description>
+  <groupId>org.apache.syncope.ext.oidcclient</groupId>
+  <artifactId>syncope-ext-oidcclient-logic</artifactId>
+  <packaging>jar</packaging>
+  
+  <properties>
+    <rootpom.basedir>${basedir}/../../..</rootpom.basedir>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.syncope.core</groupId>
+      <artifactId>syncope-core-logic</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    
+    <dependency>
+      <groupId>org.apache.syncope.ext.oidcclient</groupId>
+      <artifactId>syncope-ext-oidcclient-provisioning-java</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    
+    <dependency>
+      <groupId>org.apache.cxf</groupId>
+      <artifactId>cxf-rt-rs-extension-providers</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.cxf</groupId>
+      <artifactId>cxf-rt-rs-security-sso-oidc</artifactId>
+    </dependency>
+    
+    <dependency>
+      <groupId>com.fasterxml.jackson.jaxrs</groupId>
+      <artifactId>jackson-jaxrs-json-provider</artifactId>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-checkstyle-plugin</artifactId>
+      </plugin>
+    </plugins>
+  </build>
+</project>

http://git-wip-us.apache.org/repos/asf/syncope/blob/797fd1cb/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/OIDCClientLogic.java
----------------------------------------------------------------------
diff --git a/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/OIDCClientLogic.java b/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/OIDCClientLogic.java
new file mode 100644
index 0000000..638ae5e
--- /dev/null
+++ b/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/OIDCClientLogic.java
@@ -0,0 +1,406 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.core.logic;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.uuid.Generators;
+import com.fasterxml.uuid.impl.RandomBasedGenerator;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.ws.rs.core.MediaType;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.cxf.jaxrs.client.WebClient;
+import org.apache.cxf.jaxrs.provider.json.JsonMapObjectProvider;
+import org.apache.cxf.rs.security.jose.jaxrs.JsonWebKeysProvider;
+import org.apache.cxf.rs.security.oauth2.client.Consumer;
+import org.apache.cxf.rs.security.oauth2.common.ClientAccessToken;
+import org.apache.cxf.rs.security.oidc.common.IdToken;
+import org.apache.cxf.rs.security.oidc.common.UserInfo;
+import org.apache.cxf.rs.security.oidc.rp.IdTokenReader;
+import org.apache.cxf.rs.security.oidc.rp.UserInfoClient;
+import org.apache.syncope.common.lib.AbstractBaseBean;
+import org.apache.syncope.common.lib.OIDCConstants;
+import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.to.AttrTO;
+import org.apache.syncope.common.lib.to.OIDCLoginRequestTO;
+import org.apache.syncope.common.lib.to.OIDCLoginResponseTO;
+import org.apache.syncope.common.lib.types.CipherAlgorithm;
+import org.apache.syncope.common.lib.types.ClientExceptionType;
+import org.apache.syncope.common.lib.types.StandardEntitlement;
+import org.apache.syncope.core.logic.model.TokenEndpointResponse;
+import org.apache.syncope.core.logic.oidc.OIDCUserManager;
+import org.apache.syncope.core.persistence.api.dao.NotFoundException;
+import org.apache.syncope.core.persistence.api.dao.OIDCProviderDAO;
+import org.apache.syncope.core.persistence.api.entity.OIDCProvider;
+import org.apache.syncope.core.persistence.api.entity.OIDCProviderItem;
+import org.apache.syncope.core.provisioning.api.data.AccessTokenDataBinder;
+import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
+import org.apache.syncope.core.spring.security.AuthContextUtils;
+import org.apache.syncope.core.spring.security.AuthDataAccessor;
+import org.apache.syncope.core.spring.security.Encryptor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Component;
+
+@Component
+public class OIDCClientLogic extends AbstractTransactionalLogic<AbstractBaseBean> {
+
+    private static final Encryptor ENCRYPTOR = Encryptor.getInstance();
+
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    private static final RandomBasedGenerator UUID_GENERATOR = Generators.randomBasedGenerator();
+
+    private static final String JWT_CLAIM_OP_ENTITYID = "OP_ENTITYID";
+
+    private static final String JWT_CLAIM_USERID = "USERID";
+
+    @Autowired
+    private AuthDataAccessor authDataAccessor;
+
+    @Autowired
+    private AccessTokenDataBinder accessTokenDataBinder;
+
+    @Autowired
+    private OIDCProviderDAO opDAO;
+
+    @Autowired
+    private OIDCUserManager userManager;
+
+    private OIDCProvider getOIDCProvider(final String opName) {
+        OIDCProvider op = null;
+        if (StringUtils.isBlank(opName)) {
+            List<OIDCProvider> ops = opDAO.findAll();
+            if (!ops.isEmpty()) {
+                op = ops.get(0);
+            }
+        } else {
+            op = opDAO.findByName(opName);
+        }
+        if (op == null) {
+            throw new NotFoundException(StringUtils.isBlank(opName)
+                    ? "Any OIDC Provider"
+                    : "OIDC Provider '" + opName + "'");
+        }
+        return op;
+    }
+
+    @PreAuthorize("hasRole('" + StandardEntitlement.ANONYMOUS + "')")
+    public OIDCLoginRequestTO createLoginRequest(final String redirectURI, final String opName) {
+        // 1. look for Provider
+        OIDCProvider op = getOIDCProvider(opName);
+
+        // 2. create AuthnRequest
+        OIDCLoginRequestTO requestTO = new OIDCLoginRequestTO();
+        requestTO.setProviderAddress(op.getAuthorizationEndpoint());
+        requestTO.setClientId(op.getClientID());
+        requestTO.setScope("openid email profile");
+        requestTO.setResponseType("code");
+        requestTO.setRedirectURI(redirectURI);
+        requestTO.setState(UUID_GENERATOR.generate().toString());
+        return requestTO;
+    }
+
+    @PreAuthorize("hasRole('" + StandardEntitlement.ANONYMOUS + "')")
+    public OIDCLoginResponseTO login(final String redirectURI, final String authorizationCode, final String opName) {
+        OIDCProvider op = getOIDCProvider(opName);
+
+        // 1. get OpenID Connect tokens
+        String body = OIDCConstants.CODE + "=" + authorizationCode
+                + "&" + OIDCConstants.CLIENT_ID + "=" + op.getClientID()
+                + "&" + OIDCConstants.CLIENT_SECRET + "=" + op.getClientSecret()
+                + "&" + OIDCConstants.REDIRECT_URI + "=" + redirectURI
+                + "&" + OIDCConstants.GRANT_TYPE + "=authorization_code";
+        TokenEndpointResponse tokenEndpointResponse = getOIDCTokens(op.getTokenEndpoint(), body);
+
+        // 1. get OpenID Connect tokens
+        Consumer consumer = new Consumer(op.getClientID(), op.getClientSecret());
+
+        // 2. validate token
+        IdToken idToken = getValidatedIdToken(op, consumer, tokenEndpointResponse.getIdToken());
+
+        // 3. extract user information
+        UserInfo userInfo = getUserInfo(op, tokenEndpointResponse.getAccessToken(), idToken, consumer);
+
+        // 4. prepare the result: find matching user (if any) and return the received attributes
+        final OIDCLoginResponseTO responseTO = new OIDCLoginResponseTO();
+        responseTO.setEmail(userInfo.getEmail());
+        responseTO.setFamilyName(userInfo.getFamilyName());
+        responseTO.setGivenName(userInfo.getGivenName());
+        responseTO.setName(userInfo.getName());
+        responseTO.setSubject(userInfo.getSubject());
+
+        String keyValue = null;
+        for (OIDCProviderItem item : op.getItems()) {
+            AttrTO attrTO = new AttrTO();
+            attrTO.setSchema(item.getExtAttrName());
+            switch (item.getExtAttrName()) {
+                case UserInfo.PREFERRED_USERNAME_CLAIM:
+                    attrTO.getValues().add(userInfo.getPreferredUserName());
+                    responseTO.getAttrs().add(attrTO);
+                    if (item.isConnObjectKey()) {
+                        keyValue = userInfo.getPreferredUserName();
+                    }
+                    break;
+
+                case UserInfo.PROFILE_CLAIM:
+                    attrTO.getValues().add(userInfo.getProfile());
+                    responseTO.getAttrs().add(attrTO);
+                    if (item.isConnObjectKey()) {
+                        keyValue = userInfo.getProfile();
+                    }
+                    break;
+
+                case UserInfo.EMAIL_CLAIM:
+                    attrTO.getValues().add(userInfo.getEmail());
+                    responseTO.getAttrs().add(attrTO);
+                    if (item.isConnObjectKey()) {
+                        keyValue = userInfo.getEmail();
+                    }
+                    break;
+
+                case UserInfo.NAME_CLAIM:
+                    attrTO.getValues().add(userInfo.getName());
+                    responseTO.getAttrs().add(attrTO);
+                    if (item.isConnObjectKey()) {
+                        keyValue = userInfo.getName();
+                    }
+                    break;
+
+                case UserInfo.FAMILY_NAME_CLAIM:
+                    attrTO.getValues().add(userInfo.getFamilyName());
+                    responseTO.getAttrs().add(attrTO);
+                    if (item.isConnObjectKey()) {
+                        keyValue = userInfo.getFamilyName();
+                    }
+                    break;
+
+                case UserInfo.MIDDLE_NAME_CLAIM:
+                    attrTO.getValues().add(userInfo.getMiddleName());
+                    responseTO.getAttrs().add(attrTO);
+                    if (item.isConnObjectKey()) {
+                        keyValue = userInfo.getMiddleName();
+                    }
+                    break;
+
+                case UserInfo.GIVEN_NAME_CLAIM:
+                    attrTO.getValues().add(userInfo.getGivenName());
+                    responseTO.getAttrs().add(attrTO);
+                    if (item.isConnObjectKey()) {
+                        keyValue = userInfo.getGivenName();
+                    }
+                    break;
+
+                case UserInfo.NICKNAME_CLAIM:
+                    attrTO.getValues().add(userInfo.getNickName());
+                    responseTO.getAttrs().add(attrTO);
+                    if (item.isConnObjectKey()) {
+                        keyValue = userInfo.getNickName();
+                    }
+                    break;
+
+                case UserInfo.GENDER_CLAIM:
+                    attrTO.getValues().add(userInfo.getGender());
+                    responseTO.getAttrs().add(attrTO);
+                    if (item.isConnObjectKey()) {
+                        keyValue = userInfo.getGender();
+                    }
+                    break;
+
+                case UserInfo.LOCALE_CLAIM:
+                    attrTO.getValues().add(userInfo.getLocale());
+                    responseTO.getAttrs().add(attrTO);
+                    if (item.isConnObjectKey()) {
+                        keyValue = userInfo.getLocale();
+                    }
+                    break;
+
+                case UserInfo.ZONEINFO_CLAIM:
+                    attrTO.getValues().add(userInfo.getZoneInfo());
+                    responseTO.getAttrs().add(attrTO);
+                    if (item.isConnObjectKey()) {
+                        keyValue = userInfo.getZoneInfo();
+                    }
+                    break;
+
+                case UserInfo.BIRTHDATE_CLAIM:
+                    attrTO.getValues().add(userInfo.getBirthDate());
+                    responseTO.getAttrs().add(attrTO);
+                    if (item.isConnObjectKey()) {
+                        keyValue = userInfo.getBirthDate();
+                    }
+                    break;
+
+                case UserInfo.PHONE_CLAIM:
+                    attrTO.getValues().add(userInfo.getPhoneNumber());
+                    responseTO.getAttrs().add(attrTO);
+                    if (item.isConnObjectKey()) {
+                        keyValue = userInfo.getPhoneNumber();
+                    }
+                    break;
+
+                case UserInfo.ADDRESS_CLAIM:
+                    attrTO.getValues().add(userInfo.getUserAddress().getFormatted());
+                    responseTO.getAttrs().add(attrTO);
+                    if (item.isConnObjectKey()) {
+                        keyValue = userInfo.getUserAddress().getFormatted();
+                    }
+                    break;
+
+                case UserInfo.UPDATED_AT_CLAIM:
+                    attrTO.getValues().add(Long.toString(userInfo.getUpdatedAt()));
+                    responseTO.getAttrs().add(attrTO);
+                    if (item.isConnObjectKey()) {
+                        keyValue = Long.toString(userInfo.getUpdatedAt());
+                    }
+                    break;
+
+                default:
+                    LOG.warn("Unsupported: {} ", item.getExtAttrName());
+            }
+        }
+
+        final List<String> matchingUsers = keyValue == null
+                ? Collections.<String>emptyList()
+                : userManager.findMatchingUser(keyValue, op.getConnObjectKeyItem().get());
+        LOG.debug("Found {} matching users for {}", matchingUsers.size(), keyValue);
+
+        String username;
+        if (matchingUsers.isEmpty()) {
+            if (op.isCreateUnmatching()) {
+                LOG.debug("No user matching {}, about to create", keyValue);
+
+                final String emailValue = userInfo.getEmail();
+                username = AuthContextUtils.execWithAuthContext(AuthContextUtils.getDomain(),
+                        () -> userManager.create(op, responseTO, emailValue));
+            } else {
+                throw new NotFoundException("User matching the provided value " + keyValue);
+            }
+        } else if (matchingUsers.size() > 1) {
+            throw new IllegalArgumentException("Several users match the provided value " + keyValue);
+        } else {
+            if (op.isUpdateMatching()) {
+                LOG.debug("About to update {} for {}", matchingUsers.get(0), keyValue);
+
+                username = AuthContextUtils.execWithAuthContext(AuthContextUtils.getDomain(),
+                        () -> userManager.update(matchingUsers.get(0), op, responseTO));
+            } else {
+                username = matchingUsers.get(0);
+            }
+
+        }
+
+        responseTO.setUsername(username);
+
+        // 5. generate JWT for further access
+        Map<String, Object> claims = new HashMap<>();
+        claims.put(JWT_CLAIM_OP_ENTITYID, idToken.getIssuer());
+        claims.put(JWT_CLAIM_USERID, idToken.getSubject());
+
+        byte[] authorities = null;
+        try {
+            authorities = ENCRYPTOR.encode(POJOHelper.serialize(
+                    authDataAccessor.getAuthorities(responseTO.getUsername())), CipherAlgorithm.AES).
+                    getBytes();
+        } catch (Exception e) {
+            LOG.error("Could not fetch authorities", e);
+        }
+
+        Pair<String, Date> accessTokenInfo =
+                accessTokenDataBinder.create(responseTO.getUsername(), claims, authorities, true);
+        responseTO.setAccessToken(accessTokenInfo.getLeft());
+        responseTO.setAccessTokenExpiryTime(accessTokenInfo.getRight());
+
+        return responseTO;
+    }
+
+    private TokenEndpointResponse getOIDCTokens(final String url, final String body) {
+        String oidcTokens = WebClient.create(url).
+                type(MediaType.APPLICATION_FORM_URLENCODED).accept(MediaType.APPLICATION_JSON).
+                post(body).
+                readEntity(String.class);
+        TokenEndpointResponse endpointResponse = null;
+        try {
+            endpointResponse = MAPPER.readValue(oidcTokens, TokenEndpointResponse.class);
+        } catch (Exception e) {
+            LOG.error("While getting the Tokens from the OP", e);
+            SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
+            sce.getElements().add(e.getMessage());
+            throw sce;
+        }
+        return endpointResponse;
+    }
+
+    private IdToken getValidatedIdToken(final OIDCProvider op, final Consumer consumer, final String jwtIdToken) {
+        IdTokenReader idTokenReader = new IdTokenReader();
+        idTokenReader.setClockOffset(10);
+        idTokenReader.setIssuerId(op.getIssuer());
+        WebClient jwkSetClient = WebClient.create(
+                op.getJwksUri(), Arrays.asList(new JsonWebKeysProvider())).
+                accept(MediaType.APPLICATION_JSON);
+        idTokenReader.setJwkSetClient(jwkSetClient);
+        IdToken idToken = null;
+        try {
+            idToken = idTokenReader.getIdToken(jwtIdToken, consumer);
+        } catch (Exception e) {
+            LOG.error("While validating the id_token", e);
+            SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
+            sce.getElements().add(e.getMessage());
+            throw sce;
+        }
+        return idToken;
+    }
+
+    private UserInfo getUserInfo(
+            final OIDCProvider op,
+            final String accessToken,
+            final IdToken idToken,
+            final Consumer consumer) {
+
+        WebClient userInfoServiceClient = WebClient.create(
+                op.getUserinfoEndpoint(), Arrays.asList(new JsonMapObjectProvider())).
+                accept(MediaType.APPLICATION_JSON);
+        ClientAccessToken clientAccessToken = new ClientAccessToken("Bearer", accessToken);
+        UserInfoClient userInfoClient = new UserInfoClient();
+        userInfoClient.setUserInfoServiceClient(userInfoServiceClient);
+        UserInfo userInfo = null;
+        try {
+            userInfo = userInfoClient.getUserInfo(clientAccessToken, idToken, consumer);
+        } catch (Exception e) {
+            LOG.error("While getting the userInfo", e);
+            SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
+            sce.getElements().add(e.getMessage());
+            throw sce;
+        }
+        return userInfo;
+    }
+
+    @Override
+    protected AbstractBaseBean resolveReference(
+            final Method method, final Object... args) throws UnresolvedReferenceException {
+
+        throw new UnresolvedReferenceException();
+    }
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/797fd1cb/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/OIDCProviderLogic.java
----------------------------------------------------------------------
diff --git a/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/OIDCProviderLogic.java b/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/OIDCProviderLogic.java
new file mode 100644
index 0000000..af8e579
--- /dev/null
+++ b/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/OIDCProviderLogic.java
@@ -0,0 +1,167 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.core.logic;
+
+import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.ws.rs.core.MediaType;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.cxf.jaxrs.client.WebClient;
+import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.to.ItemTO;
+import org.apache.syncope.common.lib.to.OIDCProviderTO;
+import org.apache.syncope.common.lib.types.ClientExceptionType;
+import org.apache.syncope.common.lib.types.OIDCClientEntitlement;
+import org.apache.syncope.core.logic.init.OIDCClientClassPathScanImplementationLookup;
+import org.apache.syncope.core.logic.model.OIDCProviderDiscoveryDocument;
+import org.apache.syncope.core.persistence.api.dao.NotFoundException;
+import org.apache.syncope.core.persistence.api.dao.OIDCProviderDAO;
+import org.apache.syncope.core.persistence.api.entity.OIDCProvider;
+import org.apache.syncope.core.provisioning.api.data.OIDCProviderDataBinder;
+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 OIDCProviderLogic extends AbstractTransactionalLogic<OIDCProviderTO> {
+
+    @Autowired
+    private OIDCProviderDAO opDAO;
+
+    @Autowired
+    private OIDCProviderDataBinder binder;
+
+    @Autowired
+    private OIDCClientClassPathScanImplementationLookup implLookup;
+
+    @PreAuthorize("isAuthenticated()")
+    public Set<String> getActionsClasses() {
+        return implLookup.getActionsClasses();
+    }
+
+    private OIDCProviderDiscoveryDocument getDiscoveryDocument(final String issuer) {
+        WebClient client = WebClient.create(
+                issuer + "/.well-known/openid-configuration", Arrays.asList(new JacksonJsonProvider())).
+                accept(MediaType.APPLICATION_JSON);
+        return client.get(OIDCProviderDiscoveryDocument.class);
+    }
+
+    @PreAuthorize("hasRole('" + OIDCClientEntitlement.OP_CREATE + "')")
+    public String createFromDiscovery(final OIDCProviderTO opTO) {
+        OIDCProviderDiscoveryDocument discoveryDocument = getDiscoveryDocument(opTO.getIssuer());
+
+        opTO.setAuthorizationEndpoint(discoveryDocument.getAuthorizationEndpoint());
+        opTO.setIssuer(discoveryDocument.getIssuer());
+        opTO.setJwksUri(discoveryDocument.getJwksUri());
+        opTO.setTokenEndpoint(discoveryDocument.getTokenEndpoint());
+        opTO.setUserinfoEndpoint(discoveryDocument.getUserinfoEndpoint());
+
+        return create(opTO);
+    }
+
+    @PreAuthorize("hasRole('" + OIDCClientEntitlement.OP_CREATE + "')")
+    public String create(final OIDCProviderTO opTO) {
+        if (opTO.getConnObjectKeyItem() == null) {
+            ItemTO connObjectKeyItem = new ItemTO();
+            connObjectKeyItem.setIntAttrName("username");
+            connObjectKeyItem.setExtAttrName("email");
+            opTO.setConnObjectKeyItem(connObjectKeyItem);
+        }
+
+        OIDCProvider provider = opDAO.save(binder.create(opTO));
+
+        return provider.getKey();
+    }
+
+    @PreAuthorize("isAuthenticated()")
+    @Transactional(readOnly = true)
+    public List<OIDCProviderTO> list() {
+        return opDAO.findAll().stream().map(binder::getOIDCProviderTO).collect(Collectors.toList());
+    }
+
+    @PreAuthorize("hasRole('" + OIDCClientEntitlement.OP_READ + "')")
+    @Transactional(readOnly = true)
+    public OIDCProviderTO read(final String key) {
+        OIDCProvider op = opDAO.find(key);
+        if (op == null) {
+            throw new NotFoundException("OIDC Provider '" + key + "'");
+        }
+        return binder.getOIDCProviderTO(op);
+    }
+
+    @PreAuthorize("hasRole('" + OIDCClientEntitlement.OP_UPDATE + "')")
+    public void update(final OIDCProviderTO oidcProviderTO) {
+        OIDCProvider oidcProvider = opDAO.find(oidcProviderTO.getKey());
+        if (oidcProvider == null) {
+            throw new NotFoundException("OIDC Provider '" + oidcProviderTO.getKey() + "'");
+        }
+
+        if (!oidcProvider.getIssuer().equals(oidcProviderTO.getIssuer())) {
+            LOG.error("Issuers do not match: expected {}, found {}",
+                    oidcProvider.getIssuer(), oidcProviderTO.getIssuer());
+            SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidEntity);
+            sce.getElements().add("Issuers do not match");
+            throw sce;
+        }
+
+        opDAO.save(binder.update(oidcProvider, oidcProviderTO));
+    }
+
+    @PreAuthorize("hasRole('" + OIDCClientEntitlement.OP_DELETE + "')")
+    public void delete(final String key) {
+        OIDCProvider op = opDAO.find(key);
+        if (op == null) {
+            throw new NotFoundException("OIDC Provider '" + key + "'");
+        }
+        opDAO.delete(key);
+    }
+
+    @Override
+    protected OIDCProviderTO 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 OIDCProviderTO) {
+                    key = ((OIDCProviderTO) args[i]).getKey();
+                }
+            }
+        }
+
+        if (key != null) {
+            try {
+                return binder.getOIDCProviderTO(opDAO.find(key));
+            } catch (Throwable ignore) {
+                LOG.debug("Unresolved reference", ignore);
+                throw new UnresolvedReferenceException(ignore);
+            }
+        }
+
+        throw new UnresolvedReferenceException();
+    }
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/797fd1cb/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/init/OIDCClientClassPathScanImplementationLookup.java
----------------------------------------------------------------------
diff --git a/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/init/OIDCClientClassPathScanImplementationLookup.java b/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/init/OIDCClientClassPathScanImplementationLookup.java
new file mode 100644
index 0000000..82d6d8a
--- /dev/null
+++ b/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/init/OIDCClientClassPathScanImplementationLookup.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.core.logic.init;
+
+import java.lang.reflect.Modifier;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import org.apache.syncope.core.persistence.api.ImplementationLookup;
+import org.apache.syncope.core.persistence.api.SyncopeLoader;
+import org.apache.syncope.core.provisioning.api.OIDCProviderActions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
+import org.springframework.core.type.filter.AssignableTypeFilter;
+import org.springframework.stereotype.Component;
+import org.springframework.util.ClassUtils;
+
+@Component
+public class OIDCClientClassPathScanImplementationLookup implements SyncopeLoader {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ImplementationLookup.class);
+
+    private static final String DEFAULT_BASE_PACKAGE = "org.apache.syncope.core";
+
+    private Set<String> actionsClasses;
+
+    @Override
+    public Integer getPriority() {
+        return Integer.MIN_VALUE;
+    }
+
+    @Override
+    public void load() {
+        actionsClasses = new HashSet<>();
+
+        ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
+        scanner.addIncludeFilter(new AssignableTypeFilter(OIDCProviderActions.class));
+
+        for (BeanDefinition bd : scanner.findCandidateComponents(DEFAULT_BASE_PACKAGE)) {
+            try {
+                Class<?> clazz = ClassUtils.resolveClassName(
+                        bd.getBeanClassName(), ClassUtils.getDefaultClassLoader());
+                boolean isAbstractClazz = Modifier.isAbstract(clazz.getModifiers());
+
+                if (OIDCProviderActions.class.isAssignableFrom(clazz) && !isAbstractClazz) {
+                    actionsClasses.add(clazz.getName());
+                }
+            } catch (Throwable t) {
+                LOG.warn("Could not inspect class {}", bd.getBeanClassName(), t);
+            }
+        }
+
+        actionsClasses = Collections.unmodifiableSet(actionsClasses);
+    }
+
+    public Set<String> getActionsClasses() {
+        return actionsClasses;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/797fd1cb/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/init/OIDCClientLoader.java
----------------------------------------------------------------------
diff --git a/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/init/OIDCClientLoader.java b/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/init/OIDCClientLoader.java
new file mode 100644
index 0000000..f24eb79
--- /dev/null
+++ b/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/init/OIDCClientLoader.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.core.logic.init;
+
+import org.apache.syncope.common.lib.types.OIDCClientEntitlement;
+import org.apache.syncope.core.persistence.api.DomainsHolder;
+import org.apache.syncope.core.persistence.api.SyncopeLoader;
+import org.apache.syncope.core.provisioning.api.EntitlementsHolder;
+import org.apache.syncope.core.spring.security.AuthContextUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+@Component
+public class OIDCClientLoader implements SyncopeLoader {
+
+    @Autowired
+    private DomainsHolder domainsHolder;
+
+    @Override
+    public Integer getPriority() {
+        return 1000;
+    }
+
+    @Override
+    public void load() {
+        EntitlementsHolder.getInstance().init(OIDCClientEntitlement.values());
+
+        for (String domain : domainsHolder.getDomains().keySet()) {
+            AuthContextUtils.execWithAuthContext(domain, new AuthContextUtils.Executable<Void>() {
+
+                @Override
+                public Void exec() {
+                    return null;
+                }
+            });
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/797fd1cb/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/model/OIDCProviderDiscoveryDocument.java
----------------------------------------------------------------------
diff --git a/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/model/OIDCProviderDiscoveryDocument.java b/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/model/OIDCProviderDiscoveryDocument.java
new file mode 100644
index 0000000..aae3f0c
--- /dev/null
+++ b/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/model/OIDCProviderDiscoveryDocument.java
@@ -0,0 +1,128 @@
+/*
+ * 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.model;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonPropertyOrder({
+    "issuer",
+    "authorization_endpoint",
+    "token_endpoint",
+    "userinfo_endpoint",
+    "end_session_endpoint",
+    "jwks_uri",
+    "registration_endpoint"
+})
+public class OIDCProviderDiscoveryDocument {
+
+    @JsonProperty("issuer")
+    private String issuer;
+
+    @JsonProperty("authorization_endpoint")
+    private String authorizationEndpoint;
+
+    @JsonProperty("token_endpoint")
+    private String tokenEndpoint;
+
+    @JsonProperty("userinfo_endpoint")
+    private String userinfoEndpoint;
+
+    @JsonProperty("end_session_endpoint")
+    private String endSessionEndpoint;
+
+    @JsonProperty("jwks_uri")
+    private String jwksUri;
+
+    @JsonProperty("registration_endpoint")
+    private String registrationEndpoint;
+
+    @JsonProperty("issuer")
+    public String getIssuer() {
+        return issuer;
+    }
+
+    @JsonProperty("issuer")
+    public void setIssuer(final String issuer) {
+        this.issuer = issuer;
+    }
+
+    @JsonProperty("authorization_endpoint")
+    public String getAuthorizationEndpoint() {
+        return authorizationEndpoint;
+    }
+
+    @JsonProperty("authorization_endpoint")
+    public void setAuthorizationEndpoint(final String authorizationEndpoint) {
+        this.authorizationEndpoint = authorizationEndpoint;
+    }
+
+    @JsonProperty("token_endpoint")
+    public String getTokenEndpoint() {
+        return tokenEndpoint;
+    }
+
+    @JsonProperty("token_endpoint")
+    public void setTokenEndpoint(final String tokenEndpoint) {
+        this.tokenEndpoint = tokenEndpoint;
+    }
+
+    @JsonProperty("userinfo_endpoint")
+    public String getUserinfoEndpoint() {
+        return userinfoEndpoint;
+    }
+
+    @JsonProperty("userinfo_endpoint")
+    public void setUserinfoEndpoint(final String userinfoEndpoint) {
+        this.userinfoEndpoint = userinfoEndpoint;
+    }
+
+    @JsonProperty("end_session_endpoint")
+    public String getEndSessionEndpoint() {
+        return endSessionEndpoint;
+    }
+
+    @JsonProperty("end_session_endpoint")
+    public void setEndSessionEndpoint(final String endSessionEndpoint) {
+        this.endSessionEndpoint = endSessionEndpoint;
+    }
+
+    @JsonProperty("jwks_uri")
+    public String getJwksUri() {
+        return jwksUri;
+    }
+
+    @JsonProperty("jwks_uri")
+    public void setJwksUri(final String jwksUri) {
+        this.jwksUri = jwksUri;
+    }
+
+    @JsonProperty("registration_endpoint")
+    public String getRegistrationEndpoint() {
+        return registrationEndpoint;
+    }
+
+    @JsonProperty("registration_endpoint")
+    public void setRegistrationEndpoint(final String registrationEndpoint) {
+        this.registrationEndpoint = registrationEndpoint;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/797fd1cb/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/model/TokenEndpointResponse.java
----------------------------------------------------------------------
diff --git a/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/model/TokenEndpointResponse.java b/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/model/TokenEndpointResponse.java
new file mode 100644
index 0000000..7908b88
--- /dev/null
+++ b/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/model/TokenEndpointResponse.java
@@ -0,0 +1,86 @@
+/*
+ * 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.model;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonPropertyOrder({
+    "access_token",
+    "id_token",
+    "expires_in",
+    "token_type"
+})
+public class TokenEndpointResponse {
+
+    @JsonProperty("access_token")
+    private String accessToken;
+
+    @JsonProperty("id_token")
+    private String idToken;
+
+    @JsonProperty("expires_in")
+    private int expiresIn;
+
+    @JsonProperty("token_type")
+    private String tokenType;
+
+    @JsonProperty("access_token")
+    public String getAccessToken() {
+        return accessToken;
+    }
+
+    @JsonProperty("access_token")
+    public void setAccessToken(final String accessToken) {
+        this.accessToken = accessToken;
+    }
+
+    @JsonProperty("id_token")
+    public String getIdToken() {
+        return idToken;
+    }
+
+    @JsonProperty("id_token")
+    public void setIdToken(final String idToken) {
+        this.idToken = idToken;
+    }
+
+    @JsonProperty("expires_in")
+    public int getExpiresIn() {
+        return expiresIn;
+    }
+
+    @JsonProperty("expires_in")
+    public void setExpiresIn(final int expiresIn) {
+        this.expiresIn = expiresIn;
+    }
+
+    @JsonProperty("token_type")
+    public String getTokenType() {
+        return tokenType;
+    }
+
+    @JsonProperty("token_type")
+    public void setTokenType(final String tokenType) {
+        this.tokenType = tokenType;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/797fd1cb/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/oidc/OIDCUserManager.java
----------------------------------------------------------------------
diff --git a/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/oidc/OIDCUserManager.java b/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/oidc/OIDCUserManager.java
new file mode 100644
index 0000000..25cb784
--- /dev/null
+++ b/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/oidc/OIDCUserManager.java
@@ -0,0 +1,292 @@
+/*
+ * 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.oidc;
+
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import org.apache.commons.lang3.SerializationUtils;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.syncope.common.lib.AnyOperations;
+import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.common.lib.patch.UserPatch;
+import org.apache.syncope.common.lib.to.AttrTO;
+import org.apache.syncope.common.lib.to.OIDCLoginResponseTO;
+import org.apache.syncope.common.lib.to.PropagationStatus;
+import org.apache.syncope.common.lib.to.UserTO;
+import org.apache.syncope.common.lib.types.AnyTypeKind;
+import org.apache.syncope.core.persistence.api.attrvalue.validation.ParsingValidationException;
+import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
+import org.apache.syncope.core.persistence.api.dao.UserDAO;
+import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.api.entity.OIDCProvider;
+import org.apache.syncope.core.persistence.api.entity.OIDCProviderItem;
+import org.apache.syncope.core.persistence.api.entity.PlainAttrValue;
+import org.apache.syncope.core.persistence.api.entity.PlainSchema;
+import org.apache.syncope.core.persistence.api.entity.user.UPlainAttrValue;
+import org.apache.syncope.core.persistence.api.entity.user.User;
+import org.apache.syncope.core.provisioning.api.IntAttrName;
+import org.apache.syncope.core.provisioning.api.OIDCProviderActions;
+import org.apache.syncope.core.provisioning.api.UserProvisioningManager;
+import org.apache.syncope.core.provisioning.api.data.ItemTransformer;
+import org.apache.syncope.core.provisioning.api.data.UserDataBinder;
+import org.apache.syncope.core.provisioning.java.IntAttrNameParser;
+import org.apache.syncope.core.provisioning.java.utils.MappingUtils;
+import org.apache.syncope.core.provisioning.java.utils.TemplateUtils;
+import org.apache.syncope.core.spring.ApplicationContextProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.support.AbstractBeanDefinition;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+
+@Component
+public class OIDCUserManager {
+
+    private static final Logger LOG = LoggerFactory.getLogger(OIDCUserManager.class);
+
+    @Autowired
+    private UserDAO userDAO;
+
+    @Autowired
+    private PlainSchemaDAO plainSchemaDAO;
+
+    @Autowired
+    private IntAttrNameParser intAttrNameParser;
+
+    @Autowired
+    private EntityFactory entityFactory;
+
+    @Autowired
+    private TemplateUtils templateUtils;
+
+    @Autowired
+    private UserProvisioningManager provisioningManager;
+
+    @Autowired
+    private UserDataBinder binder;
+
+    @Transactional(readOnly = true)
+    public List<String> findMatchingUser(final String keyValue, final OIDCProviderItem connObjectKeyItem) {
+        List<String> result = new ArrayList<>();
+
+        String transformed = keyValue;
+        for (ItemTransformer transformer : MappingUtils.getItemTransformers(connObjectKeyItem)) {
+            List<Object> output = transformer.beforePull(
+                    null,
+                    null,
+                    Collections.<Object>singletonList(transformed));
+            if (output != null && !output.isEmpty()) {
+                transformed = output.get(0).toString();
+            }
+        }
+
+        IntAttrName intAttrName;
+        try {
+            intAttrName = intAttrNameParser.parse(connObjectKeyItem.getIntAttrName(), AnyTypeKind.USER);
+        } catch (ParseException e) {
+            LOG.error("Invalid intAttrName '{}' specified, ignoring", connObjectKeyItem.getIntAttrName(), e);
+            return result;
+        }
+
+        if (intAttrName.getField() != null) {
+            switch (intAttrName.getField()) {
+                case "key":
+                    User byKey = userDAO.find(transformed);
+                    if (byKey != null) {
+                        result.add(byKey.getUsername());
+                    }
+                    break;
+
+                case "username":
+                    User byUsername = userDAO.findByUsername(transformed);
+                    if (byUsername != null) {
+                        result.add(byUsername.getUsername());
+                    }
+                    break;
+
+                default:
+            }
+        } else if (intAttrName.getSchemaType() != null) {
+            switch (intAttrName.getSchemaType()) {
+                case PLAIN:
+                    PlainAttrValue value = entityFactory.newEntity(UPlainAttrValue.class);
+
+                    PlainSchema schema = plainSchemaDAO.find(intAttrName.getSchemaName());
+                    if (schema == null) {
+                        value.setStringValue(transformed);
+                    } else {
+                        try {
+                            value.parseValue(schema, transformed);
+                        } catch (ParsingValidationException e) {
+                            LOG.error("While parsing provided key value {}", transformed, e);
+                            value.setStringValue(transformed);
+                        }
+                    }
+
+                    result.addAll(userDAO.findByPlainAttrValue(intAttrName.getSchemaName(), value, false).stream().
+                            map(User::getUsername).collect(Collectors.toList()));
+                    break;
+
+                case DERIVED:
+                    result.addAll(userDAO.findByDerAttrValue(intAttrName.getSchemaName(), transformed, false).stream().
+                            map(User::getUsername).collect(Collectors.toList()));
+                    break;
+
+                default:
+            }
+        }
+
+        return result;
+    }
+
+    private List<OIDCProviderActions> getActions(final OIDCProvider op) {
+        List<OIDCProviderActions> actions = new ArrayList<>();
+
+        op.getActionsClassNames().forEach(className -> {
+            try {
+                Class<?> actionsClass = Class.forName(className);
+                OIDCProviderActions opActions = (OIDCProviderActions) ApplicationContextProvider.getBeanFactory().
+                        createBean(actionsClass, AbstractBeanDefinition.AUTOWIRE_BY_TYPE, true);
+
+                actions.add(opActions);
+            } catch (Exception e) {
+                LOG.warn("Class '{}' not found", className, e);
+            }
+        });
+
+        return actions;
+    }
+
+    public void fill(final OIDCProvider op, final OIDCLoginResponseTO responseTO, final UserTO userTO) {
+        op.getItems().forEach(item -> {
+            List<String> values = Collections.emptyList();
+            Optional<AttrTO> oidcAttr = responseTO.getAttr(item.getExtAttrName());
+            if (oidcAttr.isPresent() && !oidcAttr.get().getValues().isEmpty()) {
+                values = oidcAttr.get().getValues();
+
+                List<Object> transformed = new ArrayList<>(values);
+                for (ItemTransformer transformer : MappingUtils.getItemTransformers(item)) {
+                    transformed = transformer.beforePull(null, userTO, transformed);
+                }
+                values.clear();
+                for (Object value : transformed) {
+                    values.add(value.toString());
+                }
+            }
+
+            IntAttrName intAttrName = null;
+            try {
+                intAttrName = intAttrNameParser.parse(item.getIntAttrName(), AnyTypeKind.USER);
+            } catch (ParseException e) {
+                LOG.error("Invalid intAttrName '{}' specified, ignoring", item.getIntAttrName(), e);
+            }
+
+            if (intAttrName != null && intAttrName.getField() != null) {
+                switch (intAttrName.getField()) {
+                    case "username":
+                        if (!values.isEmpty()) {
+                            userTO.setUsername(values.get(0));
+                        }
+                        break;
+
+                    default:
+                        LOG.warn("Unsupported: {}", intAttrName.getField());
+                }
+            } else if (intAttrName != null && intAttrName.getSchemaType() != null) {
+                switch (intAttrName.getSchemaType()) {
+                    case PLAIN:
+                        Optional<AttrTO> attr = userTO.getPlainAttr(intAttrName.getSchemaName());
+                        if (attr.isPresent()) {
+                            attr.get().getValues().clear();
+                        } else {
+                            attr = Optional.of(new AttrTO.Builder().schema(intAttrName.getSchemaName()).build());
+                            userTO.getPlainAttrs().add(attr.get());
+                        }
+                        attr.get().getValues().addAll(values);
+                        break;
+
+                    default:
+                        LOG.warn("Unsupported: {} {}", intAttrName.getSchemaType(), intAttrName.getSchemaName());
+                }
+            }
+        });
+    }
+
+    @Transactional(propagation = Propagation.REQUIRES_NEW)
+    public String create(final OIDCProvider op, final OIDCLoginResponseTO responseTO, final String email) {
+        UserTO userTO = new UserTO();
+
+        if (op.getUserTemplate() != null && op.getUserTemplate().get() != null) {
+            templateUtils.apply(userTO, op.getUserTemplate().get());
+        }
+
+        List<OIDCProviderActions> actions = getActions(op);
+        for (OIDCProviderActions action : actions) {
+            userTO = action.beforeCreate(userTO, responseTO);
+        }
+
+        fill(op, responseTO, userTO);
+
+        if (userTO.getRealm() == null) {
+            userTO.setRealm(SyncopeConstants.ROOT_REALM);
+        }
+        if (userTO.getUsername() == null) {
+            userTO.setUsername(email);
+        }
+
+        Pair<String, List<PropagationStatus>> created = provisioningManager.create(userTO, false, false);
+        userTO = binder.getUserTO(created.getKey());
+
+        for (OIDCProviderActions action : actions) {
+            userTO = action.afterCreate(userTO, responseTO);
+        }
+
+        return userTO.getUsername();
+    }
+
+    @Transactional(propagation = Propagation.REQUIRES_NEW)
+    public String update(final String username, final OIDCProvider op, final OIDCLoginResponseTO responseTO) {
+        UserTO userTO = binder.getUserTO(userDAO.findKey(username));
+        UserTO original = SerializationUtils.clone(userTO);
+
+        fill(op, responseTO, userTO);
+
+        UserPatch userPatch = AnyOperations.diff(userTO, original, true);
+
+        List<OIDCProviderActions> actions = getActions(op);
+        for (OIDCProviderActions action : actions) {
+            userPatch = action.beforeUpdate(userPatch, responseTO);
+        }
+
+        Pair<UserPatch, List<PropagationStatus>> updated = provisioningManager.update(userPatch, false);
+        userTO = binder.getUserTO(updated.getLeft().getKey());
+
+        for (OIDCProviderActions action : actions) {
+            userTO = action.afterUpdate(userTO, responseTO);
+        }
+
+        return userTO.getUsername();
+    }
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/797fd1cb/ext/oidcclient/persistence-api/pom.xml
----------------------------------------------------------------------
diff --git a/ext/oidcclient/persistence-api/pom.xml b/ext/oidcclient/persistence-api/pom.xml
new file mode 100644
index 0000000..321602b
--- /dev/null
+++ b/ext/oidcclient/persistence-api/pom.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.syncope.ext</groupId>
+    <artifactId>syncope-ext-oidcclient</artifactId>
+    <version>2.1.0-SNAPSHOT</version>
+  </parent>
+
+  <name>Apache Syncope Ext: OIDC Client Persistence API</name>
+  <description>Apache Syncope Ext: OIDC Client Persistence API</description>
+  <groupId>org.apache.syncope.ext.oidcclient</groupId>
+  <artifactId>syncope-ext-oidcclient-persistence-api</artifactId>
+  <packaging>jar</packaging>
+  
+  <properties>
+    <rootpom.basedir>${basedir}/../../..</rootpom.basedir>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.syncope.core</groupId>
+      <artifactId>syncope-core-persistence-api</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.syncope.ext.oidcclient</groupId>
+      <artifactId>syncope-ext-oidcclient-common-lib</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-checkstyle-plugin</artifactId>
+      </plugin>
+    </plugins>
+  </build>
+</project>

http://git-wip-us.apache.org/repos/asf/syncope/blob/797fd1cb/ext/oidcclient/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/OIDCProviderDAO.java
----------------------------------------------------------------------
diff --git a/ext/oidcclient/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/OIDCProviderDAO.java b/ext/oidcclient/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/OIDCProviderDAO.java
new file mode 100644
index 0000000..793e461
--- /dev/null
+++ b/ext/oidcclient/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/OIDCProviderDAO.java
@@ -0,0 +1,35 @@
+/*
+ * 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;
+
+import java.util.List;
+import org.apache.syncope.core.persistence.api.entity.OIDCProvider;
+
+public interface OIDCProviderDAO {
+
+    OIDCProvider find(String key);
+
+    OIDCProvider findByName(String name);
+
+    List<OIDCProvider> findAll();
+
+    OIDCProvider save(final OIDCProvider op);
+
+    void delete(String key);
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/797fd1cb/ext/oidcclient/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/OIDCEntityFactory.java
----------------------------------------------------------------------
diff --git a/ext/oidcclient/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/OIDCEntityFactory.java b/ext/oidcclient/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/OIDCEntityFactory.java
new file mode 100644
index 0000000..489ce86
--- /dev/null
+++ b/ext/oidcclient/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/OIDCEntityFactory.java
@@ -0,0 +1,25 @@
+/*
+ * 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;
+
+public interface OIDCEntityFactory {
+
+    <E extends Entity> E newEntity(Class<E> reference);
+
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/797fd1cb/ext/oidcclient/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/OIDCProvider.java
----------------------------------------------------------------------
diff --git a/ext/oidcclient/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/OIDCProvider.java b/ext/oidcclient/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/OIDCProvider.java
new file mode 100644
index 0000000..b0c7dee
--- /dev/null
+++ b/ext/oidcclient/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/OIDCProvider.java
@@ -0,0 +1,85 @@
+/*
+ * 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;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+public interface OIDCProvider extends Entity {
+
+    String getName();
+
+    void setName(String entityID);
+
+    String getClientID();
+
+    void setClientID(String clientId);
+
+    String getClientSecret();
+
+    void setClientSecret(String clientSecret);
+
+    String getAuthorizationEndpoint();
+
+    void setAuthorizationEndpoint(String authorizationEndpoint);
+
+    String getTokenEndpoint();
+
+    void setTokenEndpoint(String tokenEndpoint);
+
+    String getJwksUri();
+
+    void setJwksUri(String jwsUri);
+
+    String getIssuer();
+
+    void setIssuer(String issuer);
+
+    String getUserinfoEndpoint();
+
+    void setUserinfoEndpoint(String userinfoEndpoint);
+
+    boolean getHasDiscovery();
+
+    void setHasDiscovery(boolean hasDiscovery);
+
+    boolean isCreateUnmatching();
+
+    void setCreateUnmatching(boolean createUnmatching);
+
+    boolean isUpdateMatching();
+
+    void setUpdateMatching(boolean updateMatching);
+
+    OIDCUserTemplate getUserTemplate();
+
+    void setUserTemplate(OIDCUserTemplate userTemplate);
+
+    List<? extends OIDCProviderItem> getItems();
+
+    Optional<? extends OIDCProviderItem> getConnObjectKeyItem();
+
+    void setConnObjectKeyItem(OIDCProviderItem item);
+
+    boolean add(OIDCProviderItem item);
+
+    Set<String> getActionsClassNames();
+
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/797fd1cb/ext/oidcclient/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/OIDCProviderItem.java
----------------------------------------------------------------------
diff --git a/ext/oidcclient/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/OIDCProviderItem.java b/ext/oidcclient/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/OIDCProviderItem.java
new file mode 100644
index 0000000..3047c88
--- /dev/null
+++ b/ext/oidcclient/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/OIDCProviderItem.java
@@ -0,0 +1,29 @@
+/*
+ * 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;
+
+import org.apache.syncope.core.persistence.api.entity.resource.Item;
+
+public interface OIDCProviderItem extends Item {
+
+    OIDCProvider getOP();
+
+    void setOP(OIDCProvider op);
+
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/797fd1cb/ext/oidcclient/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/OIDCUserTemplate.java
----------------------------------------------------------------------
diff --git a/ext/oidcclient/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/OIDCUserTemplate.java b/ext/oidcclient/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/OIDCUserTemplate.java
new file mode 100644
index 0000000..339f6ec
--- /dev/null
+++ b/ext/oidcclient/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/OIDCUserTemplate.java
@@ -0,0 +1,27 @@
+/*
+ * 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;
+
+public interface OIDCUserTemplate extends AnyTemplate {
+
+    OIDCProvider getOP();
+
+    void setOP(OIDCProvider op);
+
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/797fd1cb/ext/oidcclient/persistence-jpa/pom.xml
----------------------------------------------------------------------
diff --git a/ext/oidcclient/persistence-jpa/pom.xml b/ext/oidcclient/persistence-jpa/pom.xml
new file mode 100644
index 0000000..f642ef6
--- /dev/null
+++ b/ext/oidcclient/persistence-jpa/pom.xml
@@ -0,0 +1,134 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.syncope.ext</groupId>
+    <artifactId>syncope-ext-oidcclient</artifactId>
+    <version>2.1.0-SNAPSHOT</version>
+  </parent>
+
+  <name>Apache Syncope Ext: OIDC Client Persistence JPA</name>
+  <description>Apache Syncope Ext: OIDC Client Persistence JPA</description>
+  <groupId>org.apache.syncope.ext.oidcclient</groupId>
+  <artifactId>syncope-ext-oidcclient-persistence-jpa</artifactId>
+  <packaging>jar</packaging>
+  
+  <properties>
+    <rootpom.basedir>${basedir}/../../..</rootpom.basedir>
+  </properties>
+
+  <dependencies>    
+    <dependency>
+      <groupId>org.apache.syncope.core</groupId>
+      <artifactId>syncope-core-persistence-jpa</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.syncope.ext.oidcclient</groupId>
+      <artifactId>syncope-ext-oidcclient-persistence-api</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.openjpa</groupId>
+        <artifactId>openjpa-maven-plugin</artifactId>
+        <inherited>true</inherited>
+        <dependencies>
+          <dependency>
+            <groupId>com.h2database</groupId>
+            <artifactId>h2</artifactId>
+            <version>${h2.version}</version>
+          </dependency>
+        </dependencies>
+        <configuration>
+          <persistenceXmlFile>${rootpom.basedir}/core/persistence-jpa/src/main/resources/persistence-enhance.xml</persistenceXmlFile> 
+          <includes>org/apache/syncope/core/persistence/jpa/entity/**/*.class</includes>
+          <connectionDriverName>org.springframework.jdbc.datasource.DriverManagerDataSource</connectionDriverName>
+          <connectionProperties>
+            driverClassName=org.h2.Driver,
+            url=jdbc:h2:mem:syncopedb
+            username=sa,
+            password=
+          </connectionProperties>
+        </configuration>
+        <executions>
+          <execution>
+            <id>enhancer</id>
+            <phase>process-classes</phase>
+            <goals>
+              <goal>enhance</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-checkstyle-plugin</artifactId>
+      </plugin>
+    </plugins>
+
+    <testResources>
+      <testResource>
+        <directory>${rootpom.basedir}/core/persistence-jpa/src/main/resources</directory>
+        <filtering>true</filtering>        
+      </testResource>
+    </testResources>
+  </build>
+
+  <profiles>
+    <profile>
+      <id>sqlgen</id>
+      
+      <properties>
+        <skipTests>true</skipTests>
+      </properties>
+      
+      <build>
+        <defaultGoal>clean verify</defaultGoal>
+        
+        <plugins>
+          <plugin>
+            <groupId>org.apache.openjpa</groupId>
+            <artifactId>openjpa-maven-plugin</artifactId>
+            <inherited>true</inherited>
+            <executions>
+              <execution>
+                <id>sqlgenr</id>
+                <phase>process-classes</phase>
+                <goals>
+                  <goal>sql</goal>
+                </goals>
+              </execution>
+            </executions>
+          </plugin>          
+        </plugins>
+      </build>
+        
+    </profile>
+  </profiles>
+
+</project>

http://git-wip-us.apache.org/repos/asf/syncope/blob/797fd1cb/ext/oidcclient/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAOIDCProviderDAO.java
----------------------------------------------------------------------
diff --git a/ext/oidcclient/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAOIDCProviderDAO.java b/ext/oidcclient/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAOIDCProviderDAO.java
new file mode 100644
index 0000000..91e3af0
--- /dev/null
+++ b/ext/oidcclient/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAOIDCProviderDAO.java
@@ -0,0 +1,78 @@
+/*
+ * 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;
+
+import java.util.List;
+import javax.persistence.NoResultException;
+import javax.persistence.TypedQuery;
+import org.apache.syncope.core.persistence.api.dao.OIDCProviderDAO;
+import org.apache.syncope.core.persistence.api.entity.OIDCProvider;
+import org.apache.syncope.core.persistence.jpa.entity.JPAOIDCProvider;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+@Repository
+public class JPAOIDCProviderDAO extends AbstractDAO<OIDCProvider> implements OIDCProviderDAO {
+
+    @Transactional(readOnly = true)
+    @Override
+    public OIDCProvider find(final String key) {
+        return entityManager().find(JPAOIDCProvider.class, key);
+    }
+
+    @Transactional(readOnly = true)
+    @Override
+    public OIDCProvider findByName(final String name) {
+        TypedQuery<OIDCProvider> query = entityManager().createQuery(
+                "SELECT e FROM " + JPAOIDCProvider.class.getSimpleName()
+                + " e WHERE e.name = :name", OIDCProvider.class);
+        query.setParameter("name", name);
+
+        OIDCProvider result = null;
+        try {
+            result = query.getSingleResult();
+        } catch (NoResultException e) {
+            LOG.debug("No OIDC Provider found with name {}", name, e);
+        }
+
+        return result;
+    }
+
+    @Transactional(readOnly = true)
+    @Override
+    public List<OIDCProvider> findAll() {
+        TypedQuery<OIDCProvider> query = entityManager().createQuery(
+                "SELECT e FROM " + JPAOIDCProvider.class.getSimpleName() + " e", OIDCProvider.class);
+        return query.getResultList();
+    }
+
+    @Override
+    public OIDCProvider save(final OIDCProvider op) {
+        return entityManager().merge(op);
+    }
+
+    @Override
+    public void delete(final String key) {
+        OIDCProvider op = find(key);
+        if (op != null) {
+            entityManager().remove(op);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/797fd1cb/ext/oidcclient/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAOIDCEntityFactory.java
----------------------------------------------------------------------
diff --git a/ext/oidcclient/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAOIDCEntityFactory.java b/ext/oidcclient/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAOIDCEntityFactory.java
new file mode 100644
index 0000000..e5870bf
--- /dev/null
+++ b/ext/oidcclient/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAOIDCEntityFactory.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.core.persistence.jpa.entity;
+
+import org.apache.syncope.core.persistence.api.entity.Entity;
+import org.apache.syncope.core.persistence.api.entity.OIDCEntityFactory;
+import org.apache.syncope.core.persistence.api.entity.OIDCProvider;
+import org.apache.syncope.core.persistence.api.entity.OIDCProviderItem;
+import org.apache.syncope.core.persistence.api.entity.OIDCUserTemplate;
+import org.springframework.stereotype.Component;
+
+@Component
+public class JPAOIDCEntityFactory implements OIDCEntityFactory {
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <E extends Entity> E newEntity(final Class<E> reference) {
+        E result;
+
+        if (reference.equals(OIDCProvider.class)) {
+            result = (E) new JPAOIDCProvider();
+        } else if (reference.equals(OIDCProviderItem.class)) {
+            result = (E) new JPAOIDCProviderItem();
+        } else if (reference.equals(OIDCUserTemplate.class)) {
+            result = (E) new JPAOIDCUserTemplate();
+        } else {
+            throw new IllegalArgumentException("Could not find a JPA implementation of " + reference.getName());
+        }
+
+        return result;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/797fd1cb/ext/oidcclient/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAOIDCProvider.java
----------------------------------------------------------------------
diff --git a/ext/oidcclient/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAOIDCProvider.java b/ext/oidcclient/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAOIDCProvider.java
new file mode 100644
index 0000000..c32f91e
--- /dev/null
+++ b/ext/oidcclient/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAOIDCProvider.java
@@ -0,0 +1,250 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import javax.persistence.Cacheable;
+import javax.persistence.CascadeType;
+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.OneToMany;
+import javax.persistence.OneToOne;
+import javax.persistence.Table;
+import javax.validation.constraints.Max;
+import javax.validation.constraints.Min;
+import org.apache.syncope.core.persistence.api.entity.OIDCProvider;
+import org.apache.syncope.core.persistence.api.entity.OIDCProviderItem;
+import org.apache.syncope.core.persistence.api.entity.OIDCUserTemplate;
+
+@Entity
+@Table(name = JPAOIDCProvider.TABLE)
+@Cacheable
+public class JPAOIDCProvider extends AbstractGeneratedKeyEntity implements OIDCProvider {
+
+    public static final String TABLE = "OIDCProvider";
+
+    private static final long serialVersionUID = 1423093003585826403L;
+
+    @Column(unique = true, nullable = false)
+    private String name;
+
+    @Column(unique = true, nullable = false)
+    private String clientID;
+
+    @Column(unique = true, nullable = false)
+    private String clientSecret;
+
+    @Column(nullable = false)
+    private String authorizationEndpoint;
+
+    @Column(nullable = false)
+    private String tokenEndpoint;
+
+    @Column(nullable = false)
+    private String jwksUri;
+
+    @Column(nullable = false)
+    private String issuer;
+
+    @Column(nullable = false)
+    private String userinfoEndpoint;
+
+    @Column(nullable = false)
+    private boolean hasDiscovery;
+
+    @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER, mappedBy = "op")
+    private JPAOIDCUserTemplate userTemplate;
+
+    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER, mappedBy = "op")
+    private List<JPAOIDCProviderItem> items = new ArrayList<>();
+
+    @Min(0)
+    @Max(1)
+    @Column(nullable = false)
+    private Integer createUnmatching;
+
+    @Min(0)
+    @Max(1)
+    @Column(nullable = false)
+    private Integer updateMatching;
+
+    @ElementCollection(fetch = FetchType.EAGER)
+    @Column(name = "actionClassName")
+    @CollectionTable(name = TABLE + "_actionsClassNames",
+            joinColumns =
+            @JoinColumn(name = "oidcOP_id", referencedColumnName = "id"))
+    private Set<String> actionsClassNames = new HashSet<>();
+
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    @Override
+    public void setName(final String name) {
+        this.name = name;
+    }
+
+    @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 String getAuthorizationEndpoint() {
+        return authorizationEndpoint;
+    }
+
+    @Override
+    public void setAuthorizationEndpoint(final String authorizationEndpoint) {
+        this.authorizationEndpoint = authorizationEndpoint;
+    }
+
+    @Override
+    public String getTokenEndpoint() {
+        return tokenEndpoint;
+    }
+
+    @Override
+    public void setTokenEndpoint(final String tokenEndpoint) {
+        this.tokenEndpoint = tokenEndpoint;
+    }
+
+    @Override
+    public String getJwksUri() {
+        return jwksUri;
+    }
+
+    @Override
+    public void setJwksUri(final String jwksUri) {
+        this.jwksUri = jwksUri;
+    }
+
+    @Override
+    public String getIssuer() {
+        return issuer;
+    }
+
+    @Override
+    public void setIssuer(final String issuer) {
+        this.issuer = issuer;
+    }
+
+    @Override
+    public String getUserinfoEndpoint() {
+        return userinfoEndpoint;
+    }
+
+    @Override
+    public void setUserinfoEndpoint(final String userinfoEndpoint) {
+        this.userinfoEndpoint = userinfoEndpoint;
+    }
+
+    @Override
+    public boolean getHasDiscovery() {
+        return hasDiscovery;
+    }
+
+    @Override
+    public void setHasDiscovery(final boolean hasDiscovery) {
+        this.hasDiscovery = hasDiscovery;
+    }
+
+    @Override
+    public boolean isCreateUnmatching() {
+        return isBooleanAsInteger(createUnmatching);
+    }
+
+    @Override
+    public void setCreateUnmatching(final boolean createUnmatching) {
+        this.createUnmatching = getBooleanAsInteger(createUnmatching);
+    }
+
+    @Override
+    public boolean isUpdateMatching() {
+        return isBooleanAsInteger(updateMatching);
+    }
+
+    @Override
+    public void setUpdateMatching(final boolean updateMatching) {
+        this.updateMatching = getBooleanAsInteger(updateMatching);
+    }
+
+    @Override
+    public OIDCUserTemplate getUserTemplate() {
+        return userTemplate;
+    }
+
+    @Override
+    public void setUserTemplate(final OIDCUserTemplate userTemplate) {
+        checkType(userTemplate, JPAOIDCUserTemplate.class);
+        this.userTemplate = (JPAOIDCUserTemplate) userTemplate;
+    }
+
+    @Override
+    public boolean add(final OIDCProviderItem item) {
+        checkType(item, JPAOIDCProviderItem.class);
+        return items.contains((JPAOIDCProviderItem) item) || items.add((JPAOIDCProviderItem) item);
+    }
+
+    @Override
+    public List<? extends OIDCProviderItem> getItems() {
+        return items;
+    }
+
+    @Override
+    public Optional<? extends OIDCProviderItem> getConnObjectKeyItem() {
+        return getItems().stream().filter(item -> item.isConnObjectKey()).findFirst();
+    }
+
+    @Override
+    public void setConnObjectKeyItem(final OIDCProviderItem item) {
+        item.setConnObjectKey(true);
+        this.add(item);
+    }
+
+    @Override
+    public Set<String> getActionsClassNames() {
+        return actionsClassNames;
+    }
+
+}