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

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


The following commit(s) were added to refs/heads/master by this push:
     new e2038a4  SYNCOPE-1556: Manage OIDC JWKS (#196)
e2038a4 is described below

commit e2038a4d278501b97f9fad35b5ca9ff751d8fb77
Author: Misagh Moayyed <[email protected]>
AuthorDate: Fri Jun 12 20:22:30 2020 +0430

    SYNCOPE-1556: Manage OIDC JWKS (#196)
---
 .../apache/syncope/common/lib/to/OIDCJWKSTO.java   | 103 +++++++++++++++++++++
 .../syncope/common/lib/types/AMEntitlement.java    |   8 ++
 .../rest/api/service/OIDCJWKSConfService.java      |  54 +++++++++++
 .../rest/api/service/wa/OIDCJWKSService.java       |  68 ++++++++++++++
 .../core/logic/GoogleMfaAuthAccountLogic.java      |   4 +-
 .../core/logic/GoogleMfaAuthTokenLogic.java        |   4 +-
 .../apache/syncope/core/logic/OIDCJWKSLogic.java   |  91 ++++++++++++++++++
 .../syncope/core/logic/SAML2IdPMetadataLogic.java  |   6 +-
 .../rest/cxf/service/OIDCJWKSConfServiceImpl.java} |  27 ++++--
 .../rest/cxf/service/wa/OIDCJWKSServiceImpl.java   |  53 +++++++++++
 .../persistence/api/dao/auth/OIDCJWKSDAO.java}     |  16 ++--
 .../persistence/api/entity/auth/OIDCJWKS.java}     |  13 +--
 core/persistence-jpa-json/pom.xml                  |   5 +
 core/persistence-jpa/pom.xml                       |   5 +
 .../persistence/jpa/dao/auth/JPAOIDCJWKSDAO.java   |  58 ++++++++++++
 .../persistence/jpa/entity/JPAEntityFactory.java   |   4 +
 .../persistence/jpa/entity/auth/JPAOIDCJWKS.java}  |  32 +++++--
 .../core/persistence/jpa/inner/OIDCJWKSTest.java   |  58 ++++++++++++
 ...MetadataBinder.java => OIDCJWKSDataBinder.java} |  15 ++-
 ...Binder.java => SAML2IdPMetadataDataBinder.java} |   2 +-
 core/provisioning-java/pom.xml                     |   5 +
 .../java/data/OIDCJWKSDataBinderImpl.java          |  67 ++++++++++++++
 ...pl.java => SAML2IdPMetadataDataBinderImpl.java} |   4 +-
 .../org/apache/syncope/fit/AbstractITCase.java     |   8 ++
 .../syncope/fit/core/OIDCJWKSConfITCase.java       |  79 ++++++++++++++++
 .../apache/syncope/fit/core/OIDCJWKSITCase.java    |  54 +++++++++++
 pom.xml                                            |  17 ++++
 wa/starter/pom.xml                                 |   8 ++
 .../wa/starter/config/SyncopeWAConfiguration.java  |   8 ++
 .../oidc/SyncopeWAOIDCJWKSGeneratorService.java    |  65 +++++++++++++
 30 files changed, 891 insertions(+), 50 deletions(-)

diff --git 
a/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/OIDCJWKSTO.java 
b/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/OIDCJWKSTO.java
new file mode 100644
index 0000000..abbcbde
--- /dev/null
+++ 
b/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/OIDCJWKSTO.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.common.lib.to;
+
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+
+public class OIDCJWKSTO implements EntityTO {
+
+    private static final long serialVersionUID = 1285073386484048953L;
+
+    private String key;
+
+    private String json;
+
+    public String getJson() {
+        return json;
+    }
+
+    public void setJson(final String json) {
+        this.json = json;
+    }
+
+    public String getKey() {
+        return key;
+    }
+
+    public void setKey(final String key) {
+        this.key = key;
+    }
+
+    @Override
+    public int hashCode() {
+        return new HashCodeBuilder()
+            .appendSuper(super.hashCode())
+            .append(key)
+            .append(json)
+            .toHashCode();
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (obj == this) {
+            return true;
+        }
+        if (obj.getClass() != getClass()) {
+            return false;
+        }
+        OIDCJWKSTO rhs = (OIDCJWKSTO) obj;
+        return new EqualsBuilder()
+            .appendSuper(super.equals(obj))
+            .append(this.key, rhs.key)
+            .append(this.json, rhs.json)
+            .isEquals();
+    }
+
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this)
+            .append("key", key)
+            .append("json", json)
+            .toString();
+    }
+
+    public static class Builder {
+
+        private final OIDCJWKSTO instance = new OIDCJWKSTO();
+
+        public OIDCJWKSTO.Builder json(final String json) {
+            instance.setJson(json);
+            return this;
+        }
+
+        public OIDCJWKSTO.Builder key(final String key) {
+            instance.setKey(key);
+            return this;
+        }
+
+        public OIDCJWKSTO build() {
+            return instance;
+        }
+    }
+}
diff --git 
a/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/AMEntitlement.java
 
b/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/AMEntitlement.java
index 6577bd9..6e2ccaa 100644
--- 
a/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/AMEntitlement.java
+++ 
b/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/AMEntitlement.java
@@ -96,6 +96,14 @@ public final class AMEntitlement {
 
     public static final String GOOGLE_MFA_COUNT_ACCOUNTS = 
"GOOGLE_MFA_COUNT_ACCOUNTS";
 
+    public static final String OIDC_JWKS_CREATE = "OIDC_JWKS_CREATE";
+
+    public static final String OIDC_JWKS_READ = "OIDC_JWKS_READ";
+
+    public static final String OIDC_JWKS_UPDATE = "OIDC_JWKS_UPDATE";
+
+    public static final String OIDC_JWKS_DELETE = "OIDC_JWKS_DELETE";
+
     private static final Set<String> VALUES;
 
     static {
diff --git 
a/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/OIDCJWKSConfService.java
 
b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/OIDCJWKSConfService.java
new file mode 100644
index 0000000..fbc8de7
--- /dev/null
+++ 
b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/OIDCJWKSConfService.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.common.rest.api.service;
+
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+import io.swagger.v3.oas.annotations.security.SecurityRequirements;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.apache.syncope.common.lib.to.OIDCJWKSTO;
+import org.apache.syncope.common.rest.api.RESTHeaders;
+
+import javax.validation.constraints.NotNull;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+@Tag(name = "OIDC Json Web Keystore")
+@SecurityRequirements({
+    @SecurityRequirement(name = "BasicAuthentication"),
+    @SecurityRequirement(name = "Bearer")})
+@Path("oidc/jwks")
+public interface OIDCJWKSConfService extends JAXRSService {
+
+    @ApiResponse(responseCode = "204", description = "Operation was 
successful")
+    @PUT
+    @Consumes({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, 
MediaType.APPLICATION_XML})
+    @Produces({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, 
MediaType.APPLICATION_XML})
+    void update(@NotNull OIDCJWKSTO jwksTO);
+
+    @DELETE
+    @Consumes({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, 
MediaType.APPLICATION_XML })
+    @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, 
MediaType.APPLICATION_XML })
+    Response delete();
+}
diff --git 
a/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/OIDCJWKSService.java
 
b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/OIDCJWKSService.java
new file mode 100644
index 0000000..6c28c9a
--- /dev/null
+++ 
b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/OIDCJWKSService.java
@@ -0,0 +1,68 @@
+/*
+ * 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.wa;
+
+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 org.apache.syncope.common.lib.to.OIDCJWKSTO;
+import org.apache.syncope.common.rest.api.RESTHeaders;
+import org.apache.syncope.common.rest.api.service.JAXRSService;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+@Tag(name = "OIDC Json Web Keystore")
+@SecurityRequirements({
+    @SecurityRequirement(name = "BasicAuthentication"),
+    @SecurityRequirement(name = "Bearer")})
+@Path("wa/oidc/jwks")
+public interface OIDCJWKSService extends JAXRSService {
+
+    @GET
+    @Produces({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, 
MediaType.APPLICATION_XML})
+    OIDCJWKSTO get();
+
+    @ApiResponses({
+        @ApiResponse(responseCode = "201",
+            description = "JWKS successfully created", headers = {
+            @Header(name = RESTHeaders.RESOURCE_KEY, schema =
+            @Schema(type = "string"),
+                description = "UUID generated for the entity created"),
+            @Header(name = HttpHeaders.LOCATION, schema =
+            @Schema(type = "string"),
+                description = "URL of the entity created")}),
+        @ApiResponse(responseCode = "409",
+            description = "JWKS already exists")})
+    @POST
+    @Consumes({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, 
MediaType.APPLICATION_XML})
+    @Produces({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, 
MediaType.APPLICATION_XML})
+    Response set();
+}
diff --git 
a/core/am/logic/src/main/java/org/apache/syncope/core/logic/GoogleMfaAuthAccountLogic.java
 
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/GoogleMfaAuthAccountLogic.java
index a534a02..1ba9274 100644
--- 
a/core/am/logic/src/main/java/org/apache/syncope/core/logic/GoogleMfaAuthAccountLogic.java
+++ 
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/GoogleMfaAuthAccountLogic.java
@@ -29,6 +29,7 @@ import 
org.apache.syncope.core.persistence.api.dao.auth.AuthProfileDAO;
 import org.apache.syncope.core.persistence.api.entity.EntityFactory;
 import org.apache.syncope.core.persistence.api.entity.auth.AuthProfile;
 import org.apache.syncope.core.provisioning.api.data.AuthProfileDataBinder;
+import org.apache.syncope.core.spring.security.SecureRandomUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.stereotype.Component;
@@ -37,7 +38,6 @@ import 
org.springframework.transaction.annotation.Transactional;
 import java.lang.reflect.Method;
 import java.util.List;
 import java.util.Objects;
-import java.util.UUID;
 import java.util.stream.Collectors;
 
 @Component
@@ -108,7 +108,7 @@ public class GoogleMfaAuthAccountLogic extends 
AbstractTransactionalLogic<AuthPr
             });
 
         if (acct.getKey() == null) {
-            acct.setKey(UUID.randomUUID().toString());
+            acct.setKey(SecureRandomUtils.generateRandomUUID().toString());
         }
         profile.setGoogleMfaAuthAccount(acct);
         return authProfileDAO.save(profile).getGoogleMfaAuthAccount();
diff --git 
a/core/am/logic/src/main/java/org/apache/syncope/core/logic/GoogleMfaAuthTokenLogic.java
 
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/GoogleMfaAuthTokenLogic.java
index 9b43f6c..759b1d0 100644
--- 
a/core/am/logic/src/main/java/org/apache/syncope/core/logic/GoogleMfaAuthTokenLogic.java
+++ 
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/GoogleMfaAuthTokenLogic.java
@@ -22,7 +22,6 @@ import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
-import java.util.UUID;
 import java.util.function.Predicate;
 import org.apache.commons.lang3.ArrayUtils;
 import org.apache.syncope.common.lib.to.AuthProfileTO;
@@ -34,6 +33,7 @@ import 
org.apache.syncope.core.persistence.api.dao.auth.AuthProfileDAO;
 import org.apache.syncope.core.persistence.api.entity.EntityFactory;
 import org.apache.syncope.core.persistence.api.entity.auth.AuthProfile;
 import org.apache.syncope.core.provisioning.api.data.AuthProfileDataBinder;
+import org.apache.syncope.core.spring.security.SecureRandomUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.stereotype.Component;
@@ -106,7 +106,7 @@ public class GoogleMfaAuthTokenLogic extends 
AbstractTransactionalLogic<AuthProf
                 });
 
         if (token.getKey() == null) {
-            token.setKey(UUID.randomUUID().toString());
+            token.setKey(SecureRandomUtils.generateRandomUUID().toString());
         }
         profile.add(token);
         profile = authProfileDAO.save(profile);
diff --git 
a/core/am/logic/src/main/java/org/apache/syncope/core/logic/OIDCJWKSLogic.java 
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/OIDCJWKSLogic.java
new file mode 100644
index 0000000..4212427
--- /dev/null
+++ 
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/OIDCJWKSLogic.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.syncope.core.logic;
+
+import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.to.OIDCJWKSTO;
+import org.apache.syncope.common.lib.types.AMEntitlement;
+import org.apache.syncope.common.lib.types.ClientExceptionType;
+import org.apache.syncope.common.lib.types.IdRepoEntitlement;
+import org.apache.syncope.core.persistence.api.dao.NotFoundException;
+import org.apache.syncope.core.persistence.api.dao.auth.OIDCJWKSDAO;
+import org.apache.syncope.core.persistence.api.entity.auth.OIDCJWKS;
+import org.apache.syncope.core.provisioning.api.data.OIDCJWKSDataBinder;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.lang.reflect.Method;
+
+@Component
+public class OIDCJWKSLogic extends AbstractTransactionalLogic<OIDCJWKSTO> {
+
+    @Autowired
+    private OIDCJWKSDataBinder binder;
+
+    @Autowired
+    private OIDCJWKSDAO dao;
+
+    @PreAuthorize("hasRole('" + AMEntitlement.OIDC_JWKS_READ + "') "
+        + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+    @Transactional(readOnly = true)
+    public OIDCJWKSTO get() {
+        OIDCJWKS jwks = dao.get();
+        if (jwks != null) {
+            return binder.get(jwks);
+        }
+        throw new NotFoundException("OIDC JWKS not found");
+    }
+
+    @PreAuthorize("hasRole('" + AMEntitlement.OIDC_JWKS_CREATE + "') "
+        + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+    public OIDCJWKSTO set() {
+        OIDCJWKS jwks = dao.get();
+        if (jwks == null) {
+            return binder.get(dao.save(binder.create()));
+        }
+        throw SyncopeClientException.build(ClientExceptionType.EntityExists);
+    }
+
+    @PreAuthorize("hasRole('" + AMEntitlement.OIDC_JWKS_UPDATE + "')")
+    public OIDCJWKSTO update(final OIDCJWKSTO jwksTO) {
+        OIDCJWKS jwks = dao.get();
+        if (jwks == null) {
+            throw SyncopeClientException.build(ClientExceptionType.NotFound);
+        }
+        return binder.get(dao.save(binder.update(jwks, jwksTO)));
+    }
+
+    @Override
+    protected OIDCJWKSTO resolveReference(final Method method, final Object... 
args)
+        throws UnresolvedReferenceException {
+        OIDCJWKS jwks = dao.get();
+        if (jwks == null) {
+            throw new UnresolvedReferenceException();
+        }
+        return binder.get(jwks);
+    }
+
+    @PreAuthorize("hasRole('" + AMEntitlement.OIDC_JWKS_DELETE + "')")
+    public void delete() {
+         dao.delete();
+    }
+}
diff --git 
a/core/am/logic/src/main/java/org/apache/syncope/core/logic/SAML2IdPMetadataLogic.java
 
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/SAML2IdPMetadataLogic.java
index 87a0461..520d0c6 100644
--- 
a/core/am/logic/src/main/java/org/apache/syncope/core/logic/SAML2IdPMetadataLogic.java
+++ 
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/SAML2IdPMetadataLogic.java
@@ -18,8 +18,6 @@
  */
 package org.apache.syncope.core.logic;
 
-import static org.apache.syncope.core.logic.AbstractLogic.LOG;
-
 import java.lang.reflect.Method;
 import org.apache.commons.lang3.ArrayUtils;
 import org.apache.syncope.common.lib.SyncopeClientException;
@@ -30,7 +28,7 @@ import org.apache.syncope.common.lib.types.IdRepoEntitlement;
 import org.apache.syncope.core.persistence.api.dao.NotFoundException;
 import org.apache.syncope.core.persistence.api.dao.auth.SAML2IdPMetadataDAO;
 import org.apache.syncope.core.persistence.api.entity.auth.SAML2IdPMetadata;
-import org.apache.syncope.core.provisioning.api.data.SAML2IdPMetadataBinder;
+import 
org.apache.syncope.core.provisioning.api.data.SAML2IdPMetadataDataBinder;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.stereotype.Component;
@@ -40,7 +38,7 @@ import 
org.springframework.transaction.annotation.Transactional;
 public class SAML2IdPMetadataLogic extends 
AbstractTransactionalLogic<SAML2IdPMetadataTO> {
 
     @Autowired
-    private SAML2IdPMetadataBinder binder;
+    private SAML2IdPMetadataDataBinder binder;
 
     @Autowired
     private SAML2IdPMetadataDAO saml2IdPMetadataDAO;
diff --git 
a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/SAML2IdPMetadataBinder.java
 
b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/OIDCJWKSConfServiceImpl.java
similarity index 52%
copy from 
core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/SAML2IdPMetadataBinder.java
copy to 
core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/OIDCJWKSConfServiceImpl.java
index e2e190e..a6a534b 100644
--- 
a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/SAML2IdPMetadataBinder.java
+++ 
b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/OIDCJWKSConfServiceImpl.java
@@ -16,17 +16,30 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.core.provisioning.api.data;
+package org.apache.syncope.core.rest.cxf.service;
 
-import org.apache.syncope.common.lib.to.SAML2IdPMetadataTO;
-import org.apache.syncope.core.persistence.api.entity.auth.SAML2IdPMetadata;
+import org.apache.syncope.common.lib.to.OIDCJWKSTO;
+import org.apache.syncope.common.rest.api.service.OIDCJWKSConfService;
+import org.apache.syncope.core.logic.OIDCJWKSLogic;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
 
-public interface SAML2IdPMetadataBinder {
+import javax.ws.rs.core.Response;
 
-    SAML2IdPMetadata create(SAML2IdPMetadataTO saml2IdPMetadataTO);
+@Service
+public class OIDCJWKSConfServiceImpl extends AbstractServiceImpl implements 
OIDCJWKSConfService {
 
-    SAML2IdPMetadata update(SAML2IdPMetadata saml2IdPMetadata, 
SAML2IdPMetadataTO saml2IdPMetadataTO);
+    @Autowired
+    private OIDCJWKSLogic logic;
 
-    SAML2IdPMetadataTO getSAML2IdPMetadataTO(SAML2IdPMetadata 
saml2IdPMetadata);
+    @Override
+    public void update(final OIDCJWKSTO jwksTO) {
+        logic.update(jwksTO);
+    }
 
+    @Override
+    public Response delete() {
+        logic.delete();
+        return Response.noContent().build();
+    }
 }
diff --git 
a/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/wa/OIDCJWKSServiceImpl.java
 
b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/wa/OIDCJWKSServiceImpl.java
new file mode 100644
index 0000000..19df5d4
--- /dev/null
+++ 
b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/wa/OIDCJWKSServiceImpl.java
@@ -0,0 +1,53 @@
+/*
+ * 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.wa;
+
+import org.apache.syncope.common.lib.to.OIDCJWKSTO;
+import org.apache.syncope.common.rest.api.RESTHeaders;
+import org.apache.syncope.common.rest.api.service.wa.OIDCJWKSService;
+import org.apache.syncope.core.logic.OIDCJWKSLogic;
+import org.apache.syncope.core.rest.cxf.service.AbstractServiceImpl;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import javax.ws.rs.core.Response;
+
+import java.net.URI;
+
+@Service
+public class OIDCJWKSServiceImpl extends AbstractServiceImpl implements 
OIDCJWKSService {
+    @Autowired
+    private OIDCJWKSLogic logic;
+
+    @Override
+    public OIDCJWKSTO get() {
+        return logic.get();
+    }
+
+    @Override
+    public Response set() {
+        OIDCJWKSTO jwks = logic.set();
+        URI location = 
uriInfo.getAbsolutePathBuilder().path(jwks.getKey()).build();
+        return Response.created(location).
+            header(RESTHeaders.RESOURCE_KEY, jwks.getKey()).
+            entity(jwks).
+            build();
+    }
+}
diff --git 
a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/SAML2IdPMetadataBinder.java
 
b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/auth/OIDCJWKSDAO.java
similarity index 62%
copy from 
core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/SAML2IdPMetadataBinder.java
copy to 
core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/auth/OIDCJWKSDAO.java
index e2e190e..98acd8a 100644
--- 
a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/SAML2IdPMetadataBinder.java
+++ 
b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/auth/OIDCJWKSDAO.java
@@ -16,17 +16,15 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.core.provisioning.api.data;
+package org.apache.syncope.core.persistence.api.dao.auth;
 
-import org.apache.syncope.common.lib.to.SAML2IdPMetadataTO;
-import org.apache.syncope.core.persistence.api.entity.auth.SAML2IdPMetadata;
+import org.apache.syncope.core.persistence.api.dao.DAO;
+import org.apache.syncope.core.persistence.api.entity.auth.OIDCJWKS;
 
-public interface SAML2IdPMetadataBinder {
+public interface OIDCJWKSDAO extends DAO<OIDCJWKS> {
+    OIDCJWKS get();
 
-    SAML2IdPMetadata create(SAML2IdPMetadataTO saml2IdPMetadataTO);
-
-    SAML2IdPMetadata update(SAML2IdPMetadata saml2IdPMetadata, 
SAML2IdPMetadataTO saml2IdPMetadataTO);
-
-    SAML2IdPMetadataTO getSAML2IdPMetadataTO(SAML2IdPMetadata 
saml2IdPMetadata);
+    OIDCJWKS save(OIDCJWKS jwks);
 
+    void delete();
 }
diff --git 
a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/SAML2IdPMetadataBinder.java
 
b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/auth/OIDCJWKS.java
similarity index 62%
copy from 
core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/SAML2IdPMetadataBinder.java
copy to 
core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/auth/OIDCJWKS.java
index e2e190e..fe617a0 100644
--- 
a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/SAML2IdPMetadataBinder.java
+++ 
b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/auth/OIDCJWKS.java
@@ -16,17 +16,14 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.core.provisioning.api.data;
+package org.apache.syncope.core.persistence.api.entity.auth;
 
-import org.apache.syncope.common.lib.to.SAML2IdPMetadataTO;
-import org.apache.syncope.core.persistence.api.entity.auth.SAML2IdPMetadata;
+import org.apache.syncope.core.persistence.api.entity.Entity;
 
-public interface SAML2IdPMetadataBinder {
+public interface OIDCJWKS extends Entity {
 
-    SAML2IdPMetadata create(SAML2IdPMetadataTO saml2IdPMetadataTO);
+    String getJson();
 
-    SAML2IdPMetadata update(SAML2IdPMetadata saml2IdPMetadata, 
SAML2IdPMetadataTO saml2IdPMetadataTO);
-
-    SAML2IdPMetadataTO getSAML2IdPMetadataTO(SAML2IdPMetadata 
saml2IdPMetadata);
+    void setJson(String json);
 
 }
diff --git a/core/persistence-jpa-json/pom.xml 
b/core/persistence-jpa-json/pom.xml
index 83db64d..e71f5ae 100644
--- a/core/persistence-jpa-json/pom.xml
+++ b/core/persistence-jpa-json/pom.xml
@@ -86,6 +86,11 @@ under the License.
       <artifactId>junit-jupiter</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>com.nimbusds</groupId>
+      <artifactId>nimbus-jose-jwt</artifactId>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
   <build>
diff --git a/core/persistence-jpa/pom.xml b/core/persistence-jpa/pom.xml
index 0d6b810..8019673 100644
--- a/core/persistence-jpa/pom.xml
+++ b/core/persistence-jpa/pom.xml
@@ -138,6 +138,11 @@ under the License.
       <artifactId>bcprov-jdk15on</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>com.nimbusds</groupId>
+      <artifactId>nimbus-jose-jwt</artifactId>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
   <build>
diff --git 
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/auth/JPAOIDCJWKSDAO.java
 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/auth/JPAOIDCJWKSDAO.java
new file mode 100644
index 0000000..19cc1e3
--- /dev/null
+++ 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/auth/JPAOIDCJWKSDAO.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.core.persistence.jpa.dao.auth;
+
+import org.apache.syncope.core.persistence.api.dao.auth.OIDCJWKSDAO;
+import org.apache.syncope.core.persistence.api.entity.auth.OIDCJWKS;
+import org.apache.syncope.core.persistence.jpa.dao.AbstractDAO;
+import org.apache.syncope.core.persistence.jpa.entity.auth.JPAOIDCJWKS;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.persistence.NoResultException;
+import javax.persistence.TypedQuery;
+
+@Repository
+public class JPAOIDCJWKSDAO extends AbstractDAO<OIDCJWKS> implements 
OIDCJWKSDAO {
+
+    @Transactional(readOnly = true)
+    @Override
+    public OIDCJWKS get() {
+        try {
+            TypedQuery<OIDCJWKS> query = entityManager().
+                createQuery("SELECT e FROM " + 
JPAOIDCJWKS.class.getSimpleName() + " e", OIDCJWKS.class);
+            return query.getSingleResult();
+        } catch (final NoResultException e) {
+            LOG.debug(e.getMessage());
+        }
+        return null;
+    }
+
+    @Override
+    public OIDCJWKS save(final OIDCJWKS jwks) {
+        return entityManager().merge(jwks);
+    }
+
+    @Override
+    public void delete() {
+        entityManager().
+            createQuery("DELETE FROM " + JPAOIDCJWKS.class.getSimpleName()).
+            executeUpdate();
+    }
+}
diff --git 
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAEntityFactory.java
 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAEntityFactory.java
index 131709d..6f730ef 100644
--- 
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAEntityFactory.java
+++ 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAEntityFactory.java
@@ -59,6 +59,7 @@ import 
org.apache.syncope.core.persistence.api.entity.anyobject.AnyObject;
 import org.apache.syncope.core.persistence.api.entity.auth.AuthModule;
 import org.apache.syncope.core.persistence.api.entity.auth.AuthModuleItem;
 import org.apache.syncope.core.persistence.api.entity.auth.AuthProfile;
+import org.apache.syncope.core.persistence.api.entity.auth.OIDCJWKS;
 import org.apache.syncope.core.persistence.api.entity.auth.OIDCRP;
 import org.apache.syncope.core.persistence.api.entity.auth.SAML2IdPMetadata;
 import org.apache.syncope.core.persistence.api.entity.auth.SAML2SP;
@@ -114,6 +115,7 @@ import 
org.apache.syncope.core.persistence.jpa.entity.anyobject.JPAAPlainAttrVal
 import 
org.apache.syncope.core.persistence.jpa.entity.anyobject.JPAARelationship;
 import org.apache.syncope.core.persistence.jpa.entity.anyobject.JPAAnyObject;
 import org.apache.syncope.core.persistence.jpa.entity.auth.JPAAuthProfile;
+import org.apache.syncope.core.persistence.jpa.entity.auth.JPAOIDCJWKS;
 import org.apache.syncope.core.persistence.jpa.entity.auth.JPAOIDCRP;
 import org.apache.syncope.core.persistence.jpa.entity.auth.JPASAML2SP;
 import org.apache.syncope.core.persistence.jpa.entity.auth.JPASAML2SPKeystore;
@@ -337,6 +339,8 @@ public class JPAEntityFactory implements EntityFactory {
             result = (E) new JPASAML2SPKeystore();
         } else if (reference.equals(AuthProfile.class)) {
             result = (E) new JPAAuthProfile();
+        } else if (reference.equals(OIDCJWKS.class)) {
+            result = (E) new JPAOIDCJWKS();
         } else {
             throw new IllegalArgumentException("Could not find a JPA 
implementation of " + reference.getName());
         }
diff --git 
a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/SAML2IdPMetadataBinder.java
 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/auth/JPAOIDCJWKS.java
similarity index 50%
copy from 
core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/SAML2IdPMetadataBinder.java
copy to 
core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/auth/JPAOIDCJWKS.java
index e2e190e..d01b038 100644
--- 
a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/SAML2IdPMetadataBinder.java
+++ 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/auth/JPAOIDCJWKS.java
@@ -16,17 +16,35 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.core.provisioning.api.data;
+package org.apache.syncope.core.persistence.jpa.entity.auth;
 
-import org.apache.syncope.common.lib.to.SAML2IdPMetadataTO;
-import org.apache.syncope.core.persistence.api.entity.auth.SAML2IdPMetadata;
+import org.apache.syncope.core.persistence.api.entity.auth.OIDCJWKS;
+import 
org.apache.syncope.core.persistence.jpa.entity.AbstractGeneratedKeyEntity;
 
-public interface SAML2IdPMetadataBinder {
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Lob;
+import javax.persistence.Table;
 
-    SAML2IdPMetadata create(SAML2IdPMetadataTO saml2IdPMetadataTO);
+@Entity
+@Table(name = JPAOIDCJWKS.TABLE)
+public class JPAOIDCJWKS extends AbstractGeneratedKeyEntity implements 
OIDCJWKS {
 
-    SAML2IdPMetadata update(SAML2IdPMetadata saml2IdPMetadata, 
SAML2IdPMetadataTO saml2IdPMetadataTO);
+    public static final String TABLE = "OIDCJWKS";
 
-    SAML2IdPMetadataTO getSAML2IdPMetadataTO(SAML2IdPMetadata 
saml2IdPMetadata);
+    private static final long serialVersionUID = 47352617217394093L;
 
+    @Column
+    @Lob
+    private String json;
+
+    @Override
+    public String getJson() {
+        return this.json;
+    }
+
+    @Override
+    public void setJson(final String json) {
+        this.json = json;
+    }
 }
diff --git 
a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/OIDCJWKSTest.java
 
b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/OIDCJWKSTest.java
new file mode 100644
index 0000000..bd23665
--- /dev/null
+++ 
b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/OIDCJWKSTest.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.core.persistence.jpa.inner;
+
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jose.jwk.KeyUse;
+import com.nimbusds.jose.jwk.RSAKey;
+import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
+import org.apache.syncope.core.persistence.api.dao.auth.OIDCJWKSDAO;
+import org.apache.syncope.core.persistence.api.entity.auth.OIDCJWKS;
+import org.apache.syncope.core.persistence.jpa.AbstractTest;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+@Transactional("Master")
+public class OIDCJWKSTest extends AbstractTest {
+
+    @Autowired
+    private OIDCJWKSDAO jwksDAO;
+
+    @Test
+    public void save() throws Exception {
+        OIDCJWKS jwks = entityFactory.newEntity(OIDCJWKS.class);
+
+        RSAKey jwk = new RSAKeyGenerator(2048)
+            .keyUse(KeyUse.SIGNATURE)
+            .keyID(UUID.randomUUID().toString())
+            .generate();
+
+        String json = new JWKSet(jwk).toString();
+        jwks.setJson(json);
+        jwks = jwksDAO.save(jwks);
+        assertNotNull(jwks);
+        assertNotNull(jwks.getKey());
+
+    }
+}
diff --git 
a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/SAML2IdPMetadataBinder.java
 
b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/OIDCJWKSDataBinder.java
similarity index 66%
copy from 
core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/SAML2IdPMetadataBinder.java
copy to 
core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/OIDCJWKSDataBinder.java
index e2e190e..164c620 100644
--- 
a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/SAML2IdPMetadataBinder.java
+++ 
b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/OIDCJWKSDataBinder.java
@@ -18,15 +18,14 @@
  */
 package org.apache.syncope.core.provisioning.api.data;
 
-import org.apache.syncope.common.lib.to.SAML2IdPMetadataTO;
-import org.apache.syncope.core.persistence.api.entity.auth.SAML2IdPMetadata;
+import org.apache.syncope.common.lib.to.OIDCJWKSTO;
+import org.apache.syncope.core.persistence.api.entity.auth.OIDCJWKS;
 
-public interface SAML2IdPMetadataBinder {
+public interface OIDCJWKSDataBinder {
+    
+    OIDCJWKSTO get(OIDCJWKS jwks);
 
-    SAML2IdPMetadata create(SAML2IdPMetadataTO saml2IdPMetadataTO);
-
-    SAML2IdPMetadata update(SAML2IdPMetadata saml2IdPMetadata, 
SAML2IdPMetadataTO saml2IdPMetadataTO);
-
-    SAML2IdPMetadataTO getSAML2IdPMetadataTO(SAML2IdPMetadata 
saml2IdPMetadata);
+    OIDCJWKS create();
 
+    OIDCJWKS update(OIDCJWKS oidcjwks, OIDCJWKSTO jwksTO);
 }
diff --git 
a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/SAML2IdPMetadataBinder.java
 
b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/SAML2IdPMetadataDataBinder.java
similarity index 96%
rename from 
core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/SAML2IdPMetadataBinder.java
rename to 
core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/SAML2IdPMetadataDataBinder.java
index e2e190e..3cabfbb 100644
--- 
a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/SAML2IdPMetadataBinder.java
+++ 
b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/SAML2IdPMetadataDataBinder.java
@@ -21,7 +21,7 @@ package org.apache.syncope.core.provisioning.api.data;
 import org.apache.syncope.common.lib.to.SAML2IdPMetadataTO;
 import org.apache.syncope.core.persistence.api.entity.auth.SAML2IdPMetadata;
 
-public interface SAML2IdPMetadataBinder {
+public interface SAML2IdPMetadataDataBinder {
 
     SAML2IdPMetadata create(SAML2IdPMetadataTO saml2IdPMetadataTO);
 
diff --git a/core/provisioning-java/pom.xml b/core/provisioning-java/pom.xml
index 5a2211e..9ac7db6 100644
--- a/core/provisioning-java/pom.xml
+++ b/core/provisioning-java/pom.xml
@@ -97,6 +97,11 @@ under the License.
     </dependency>
 
     <dependency>
+      <groupId>com.nimbusds</groupId>
+      <artifactId>nimbus-jose-jwt</artifactId>
+    </dependency>
+
+    <dependency>
       <groupId>org.apache.syncope.core</groupId>
       <artifactId>syncope-core-workflow-api</artifactId>
       <version>${project.version}</version>
diff --git 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/OIDCJWKSDataBinderImpl.java
 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/OIDCJWKSDataBinderImpl.java
new file mode 100644
index 0000000..7ff3397
--- /dev/null
+++ 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/OIDCJWKSDataBinderImpl.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.syncope.core.provisioning.java.data;
+
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jose.jwk.KeyUse;
+import com.nimbusds.jose.jwk.RSAKey;
+import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
+import org.apache.syncope.common.lib.to.OIDCJWKSTO;
+import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.api.entity.auth.OIDCJWKS;
+import org.apache.syncope.core.provisioning.api.data.OIDCJWKSDataBinder;
+import org.apache.syncope.core.spring.security.SecureRandomUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+@Component
+public class OIDCJWKSDataBinderImpl implements OIDCJWKSDataBinder {
+    @Autowired
+    private EntityFactory entityFactory;
+
+    @Override
+    public OIDCJWKSTO get(final OIDCJWKS jwks) {
+        return new OIDCJWKSTO.Builder().
+            json(jwks.getJson()).
+            key(jwks.getKey()).
+            build();
+    }
+
+    @Override
+    public OIDCJWKS create() {
+        try {
+            OIDCJWKS jwks = entityFactory.newEntity(OIDCJWKS.class);
+            RSAKey jwk = new RSAKeyGenerator(2048)
+                .keyUse(KeyUse.SIGNATURE)
+                .keyID(SecureRandomUtils.generateRandomUUID().toString())
+                .generate();
+            jwks.setJson(new JWKSet(jwk).toString());
+            return jwks;
+        } catch (final Exception e) {
+            throw new RuntimeException("Unable to create OIDC JWKS", e);
+        }
+    }
+
+    @Override
+    public OIDCJWKS update(final OIDCJWKS oidcjwks, final OIDCJWKSTO jwksTO) {
+        oidcjwks.setJson(jwksTO.getJson());
+        return oidcjwks;
+    }
+}
diff --git 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/SAML2IdPMetadataBinderImpl.java
 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/SAML2IdPMetadataDataBinderImpl.java
similarity index 97%
rename from 
core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/SAML2IdPMetadataBinderImpl.java
rename to 
core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/SAML2IdPMetadataDataBinderImpl.java
index 1f4d6fa..2d17168 100644
--- 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/SAML2IdPMetadataBinderImpl.java
+++ 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/SAML2IdPMetadataDataBinderImpl.java
@@ -23,10 +23,10 @@ import 
org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 import org.apache.syncope.common.lib.to.SAML2IdPMetadataTO;
 import org.apache.syncope.core.persistence.api.entity.auth.SAML2IdPMetadata;
-import org.apache.syncope.core.provisioning.api.data.SAML2IdPMetadataBinder;
+import 
org.apache.syncope.core.provisioning.api.data.SAML2IdPMetadataDataBinder;
 
 @Component
-public class SAML2IdPMetadataBinderImpl implements SAML2IdPMetadataBinder {
+public class SAML2IdPMetadataDataBinderImpl implements 
SAML2IdPMetadataDataBinder {
 
     @Autowired
     private EntityFactory entityFactory;
diff --git 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java
index 3f0764d..cf081ff 100644
--- 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java
+++ 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java
@@ -113,9 +113,11 @@ import 
org.apache.syncope.common.rest.api.service.ConnectorService;
 import org.apache.syncope.common.rest.api.service.DynRealmService;
 import org.apache.syncope.common.rest.api.service.LoggerService;
 import org.apache.syncope.common.rest.api.service.NotificationService;
+import org.apache.syncope.common.rest.api.service.OIDCJWKSConfService;
 import org.apache.syncope.common.rest.api.service.SAML2SPKeystoreConfService;
 import 
org.apache.syncope.common.rest.api.service.wa.GoogleMfaAuthAccountService;
 import org.apache.syncope.common.rest.api.service.wa.GoogleMfaAuthTokenService;
+import org.apache.syncope.common.rest.api.service.wa.OIDCJWKSService;
 import org.apache.syncope.common.rest.api.service.wa.SAML2SPKeystoreService;
 import org.apache.syncope.common.rest.api.service.SAML2SPMetadataConfService;
 import org.apache.syncope.common.rest.api.service.wa.SAML2SPMetadataService;
@@ -343,6 +345,10 @@ public abstract class AbstractITCase {
 
     protected static AuthProfileService authProfileService;
 
+    protected static OIDCJWKSService oidcJwksService;
+
+    protected static OIDCJWKSConfService oidcJwksConfService;
+
     @BeforeAll
     public static void securitySetup() {
         try (InputStream propStream = 
Encryptor.class.getResourceAsStream("/security.properties")) {
@@ -423,6 +429,8 @@ public abstract class AbstractITCase {
         googleMfaAuthTokenService = 
adminClient.getService(GoogleMfaAuthTokenService.class);
         googleMfaAuthAccountService = 
adminClient.getService(GoogleMfaAuthAccountService.class);
         authProfileService = adminClient.getService(AuthProfileService.class);
+        oidcJwksService = adminClient.getService(OIDCJWKSService.class);
+        oidcJwksConfService = 
adminClient.getService(OIDCJWKSConfService.class);
     }
 
     @Autowired
diff --git 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/OIDCJWKSConfITCase.java
 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/OIDCJWKSConfITCase.java
new file mode 100644
index 0000000..8c1a70d
--- /dev/null
+++ 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/OIDCJWKSConfITCase.java
@@ -0,0 +1,79 @@
+/*
+ * 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 com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jose.jwk.KeyUse;
+import com.nimbusds.jose.jwk.RSAKey;
+import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
+import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.to.OIDCJWKSTO;
+import org.apache.syncope.common.lib.types.ClientExceptionType;
+import org.apache.syncope.fit.AbstractITCase;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.Executable;
+import org.springframework.http.HttpStatus;
+
+import javax.ws.rs.core.GenericType;
+import javax.ws.rs.core.Response;
+
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class OIDCJWKSConfITCase extends AbstractITCase {
+
+    private static OIDCJWKSTO getCurrentJwksTO() {
+        try {
+            return oidcJwksService.get();
+        } catch (final SyncopeClientException e) {
+            if (e.getType() == ClientExceptionType.NotFound) {
+                Response response = oidcJwksService.set();
+                assertEquals(HttpStatus.CREATED.value(), response.getStatus());
+                return response.readEntity(new GenericType<OIDCJWKSTO>() {
+                });
+            }
+        }
+        throw new RuntimeException("Unable to locate current OIDC JWKS");
+    }
+
+    @Test
+    public void verifyJwks() throws Exception {
+        oidcJwksConfService.delete();
+
+        RSAKey jwk = new RSAKeyGenerator(2048)
+            .keyUse(KeyUse.SIGNATURE)
+            .keyID(UUID.randomUUID().toString())
+            .generate();
+        String json = new JWKSet(jwk).toString();
+
+        assertDoesNotThrow(new Executable() {
+            @Override
+            public void execute() {
+                OIDCJWKSTO currentTO = getCurrentJwksTO();
+                currentTO.setJson(json);
+                oidcJwksConfService.update(currentTO);
+            }
+        });
+        OIDCJWKSTO currentTO = getCurrentJwksTO();
+        assertEquals(json, currentTO.getJson());
+    }
+
+}
diff --git 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/OIDCJWKSITCase.java
 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/OIDCJWKSITCase.java
new file mode 100644
index 0000000..931213f
--- /dev/null
+++ 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/OIDCJWKSITCase.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.fit.core;
+
+import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.types.ClientExceptionType;
+import org.apache.syncope.fit.AbstractITCase;
+import org.junit.jupiter.api.Test;
+import org.springframework.http.HttpStatus;
+
+import javax.ws.rs.core.Response;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class OIDCJWKSITCase extends AbstractITCase {
+
+    @Test
+    public void verifyJwks() {
+        try {
+            oidcJwksConfService.delete();
+
+            oidcJwksService.get();
+            fail("Should not locate an OIDC JWKS");
+        } catch (final SyncopeClientException e) {
+            assertEquals(ClientExceptionType.NotFound, e.getType());
+        }
+        Response response = oidcJwksService.set();
+        assertEquals(HttpStatus.CREATED.value(), response.getStatus());
+        try {
+            oidcJwksService.set();
+            fail("Should not recreate an OIDC JWKS");
+        } catch (final SyncopeClientException e) {
+            assertEquals(ClientExceptionType.EntityExists, e.getType());
+        }
+    }
+
+}
diff --git a/pom.xml b/pom.xml
index 0e7d626..ae58642 100644
--- a/pom.xml
+++ b/pom.xml
@@ -452,6 +452,8 @@ under the License.
 
     <h2.version>1.4.200</h2.version>
 
+    <nimbus.jose.version>8.19</nimbus.jose.version>
+
     <junit.version>5.6.2</junit.version>
     <mockito.version>3.3.0</mockito.version>
 
@@ -1607,6 +1609,16 @@ under the License.
       </dependency>
       <dependency>
         <groupId>org.apereo.cas</groupId>
+        <artifactId>cas-server-support-oidc-core-api</artifactId>
+        <version>${cas.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apereo.cas</groupId>
+        <artifactId>cas-server-support-oidc-core</artifactId>
+        <version>${cas.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apereo.cas</groupId>
         <artifactId>cas-server-support-oauth-services</artifactId>
         <version>${cas.version}</version>
       </dependency>
@@ -1993,6 +2005,11 @@ under the License.
         <artifactId>bcprov-jdk15on</artifactId>
         <version>${bouncycastle.version}</version>
       </dependency>
+      <dependency>
+        <groupId>com.nimbusds</groupId>
+        <artifactId>nimbus-jose-jwt</artifactId>
+        <version>${nimbus.jose.version}</version>
+      </dependency>
 
       <!-- TEST -->
       <dependency>
diff --git a/wa/starter/pom.xml b/wa/starter/pom.xml
index 16e7126..419dfd3 100644
--- a/wa/starter/pom.xml
+++ b/wa/starter/pom.xml
@@ -180,6 +180,14 @@ under the License.
     </dependency>
     <dependency>
       <groupId>org.apereo.cas</groupId>
+      <artifactId>cas-server-support-oidc-core-api</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apereo.cas</groupId>
+      <artifactId>cas-server-support-oidc-core</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apereo.cas</groupId>
       <artifactId>cas-server-support-oauth-services</artifactId>
     </dependency>
     <dependency>
diff --git 
a/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/SyncopeWAConfiguration.java
 
b/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/SyncopeWAConfiguration.java
index be14196..0cce830 100644
--- 
a/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/SyncopeWAConfiguration.java
+++ 
b/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/SyncopeWAConfiguration.java
@@ -20,6 +20,7 @@ package org.apache.syncope.wa.starter.config;
 
 import org.apereo.cas.audit.AuditTrailExecutionPlanConfigurer;
 import org.apereo.cas.configuration.CasConfigurationProperties;
+import org.apereo.cas.oidc.jwks.OidcJsonWebKeystoreGeneratorService;
 import 
org.apereo.cas.otp.repository.credentials.OneTimeTokenCredentialRepository;
 import org.apereo.cas.otp.repository.token.OneTimeTokenRepository;
 import org.apereo.cas.services.ServiceRegistryExecutionPlanConfigurer;
@@ -48,6 +49,7 @@ import org.apache.syncope.wa.starter.mapping.AuthMapper;
 import org.apache.syncope.wa.starter.mapping.ClientAppMapFor;
 import org.apache.syncope.wa.starter.mapping.ClientAppMapper;
 import org.apache.syncope.wa.starter.mapping.RegisteredServiceMapper;
+import org.apache.syncope.wa.starter.oidc.SyncopeWAOIDCJWKSGeneratorService;
 import org.apache.syncope.wa.starter.pac4j.saml.SyncopeWASAML2ClientCustomizer;
 import 
org.apache.syncope.wa.starter.saml.idp.metadata.RestfulSamlIdPMetadataGenerator;
 import 
org.apache.syncope.wa.starter.saml.idp.metadata.RestfulSamlIdPMetadataLocator;
@@ -189,6 +191,12 @@ public class SyncopeWAConfiguration {
     }
 
     @Bean
+    @Autowired
+    public OidcJsonWebKeystoreGeneratorService 
oidcJsonWebKeystoreGeneratorService(final WARestClient restClient) {
+        return new SyncopeWAOIDCJWKSGeneratorService(restClient);
+    }
+
+    @Bean
     public KeymasterStart keymasterStart() {
         return new KeymasterStart(NetworkService.Type.WA);
     }
diff --git 
a/wa/starter/src/main/java/org/apache/syncope/wa/starter/oidc/SyncopeWAOIDCJWKSGeneratorService.java
 
b/wa/starter/src/main/java/org/apache/syncope/wa/starter/oidc/SyncopeWAOIDCJWKSGeneratorService.java
new file mode 100644
index 0000000..346f504
--- /dev/null
+++ 
b/wa/starter/src/main/java/org/apache/syncope/wa/starter/oidc/SyncopeWAOIDCJWKSGeneratorService.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.syncope.wa.starter.oidc;
+
+import org.apereo.cas.oidc.jwks.OidcJsonWebKeystoreGeneratorService;
+
+import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.to.OIDCJWKSTO;
+import org.apache.syncope.common.lib.types.ClientExceptionType;
+import org.apache.syncope.common.rest.api.service.wa.OIDCJWKSService;
+import org.apache.syncope.wa.bootstrap.WARestClient;
+import org.springframework.core.io.ByteArrayResource;
+import org.springframework.core.io.Resource;
+
+import javax.ws.rs.core.GenericType;
+import javax.ws.rs.core.Response;
+
+import java.nio.charset.StandardCharsets;
+
+public class SyncopeWAOIDCJWKSGeneratorService implements 
OidcJsonWebKeystoreGeneratorService {
+    private final WARestClient waRestClient;
+
+    public SyncopeWAOIDCJWKSGeneratorService(final WARestClient restClient) {
+        this.waRestClient = restClient;
+    }
+
+    @Override
+    public Resource generate() {
+        if (!WARestClient.isReady()) {
+            throw new RuntimeException("Syncope core is not yet ready");
+        }
+
+        OIDCJWKSService service = waRestClient.getSyncopeClient().
+            getService(OIDCJWKSService.class);
+        try {
+            Response response = service.set();
+            OIDCJWKSTO jwksTO = response.readEntity(new 
GenericType<OIDCJWKSTO>() {
+            });
+            return new 
ByteArrayResource(jwksTO.getJson().getBytes(StandardCharsets.UTF_8), "OIDC 
JWKS");
+        } catch (final SyncopeClientException e) {
+            if (e.getType() == ClientExceptionType.EntityExists) {
+                OIDCJWKSTO jwksTO = service.get();
+                return new 
ByteArrayResource(jwksTO.getJson().getBytes(StandardCharsets.UTF_8), "OIDC 
JWKS");
+            }
+        }
+        throw new RuntimeException("Unable to determine OIDC JWKS resource");
+    }
+}

Reply via email to