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

ilgrosso pushed a commit to branch 4_0_X
in repository https://gitbox.apache.org/repos/asf/syncope.git


The following commit(s) were added to refs/heads/4_0_X by this push:
     new 7ed381fcf4 [SYNCOPE-1936] Generate OIDC JWKS as CAS does (#1252)
7ed381fcf4 is described below

commit 7ed381fcf4f54c6fe93e651c307c7c4df945d6c7
Author: Francesco Chicchiriccò <[email protected]>
AuthorDate: Tue Dec 2 19:39:54 2025 +0100

    [SYNCOPE-1936] Generate OIDC JWKS as CAS does (#1252)
---
 .../apache/syncope/client/console/panels/OIDC.java |  41 ++++---
 .../console/panels/OIDCJWKSGenerationPanel.java    | 117 +++++++++++++++++++
 .../client/console/rest/OIDCJWKSRestClient.java    |   4 +-
 .../apache/syncope/client/console/panels/OIDC.html |   1 +
 .../console/panels/OIDCJWKSGenerationPanel.html    |  27 +++++
 .../apache/syncope/core/logic/AMLogicContext.java  |   5 +-
 .../apache/syncope/core/logic/OIDCJWKSLogic.java   |  53 ++++++---
 .../core/persistence/jpa/dao/JPAOIDCJWKSDAO.java   |   8 +-
 .../provisioning/api/data/OIDCJWKSDataBinder.java  |  30 +++++
 core/provisioning-java/pom.xml                     |   5 +
 .../java/data/OIDCJWKSDataBinderImpl.java          | 130 ++++++++++-----------
 docker/core/LICENSE                                |   4 +
 pom.xml                                            |   6 +
 .../syncope/wa/starter/config/WAContext.java       |   6 +-
 .../starter/oidc/WAOIDCJWKSGeneratorService.java   |  16 ++-
 15 files changed, 344 insertions(+), 109 deletions(-)

diff --git 
a/client/am/console/src/main/java/org/apache/syncope/client/console/panels/OIDC.java
 
b/client/am/console/src/main/java/org/apache/syncope/client/console/panels/OIDC.java
index 8a4f25cbc3..b71c26bf7a 100644
--- 
a/client/am/console/src/main/java/org/apache/syncope/client/console/panels/OIDC.java
+++ 
b/client/am/console/src/main/java/org/apache/syncope/client/console/panels/OIDC.java
@@ -25,6 +25,7 @@ import java.util.Optional;
 import org.apache.commons.lang3.mutable.Mutable;
 import org.apache.syncope.client.console.SyncopeConsoleSession;
 import org.apache.syncope.client.console.rest.OIDCJWKSRestClient;
+import org.apache.syncope.client.console.rest.WAConfigRestClient;
 import 
org.apache.syncope.client.console.wicket.markup.html.bootstrap.dialog.BaseModal;
 import 
org.apache.syncope.client.console.wicket.markup.html.form.JsonEditorPanel;
 import org.apache.syncope.client.ui.commons.Constants;
@@ -55,6 +56,11 @@ public class OIDC extends Panel {
     @SpringBean
     protected OIDCJWKSRestClient oidcJWKSRestClient;
 
+    @SpringBean
+    protected WAConfigRestClient waConfigRestClient;
+
+    protected final BaseModal<OIDCJWKSTO> generateModal = new 
BaseModal<>("generateModal");
+
     protected final BaseModal<String> viewModal = new BaseModal<>("viewModal") 
{
 
         private static final long serialVersionUID = 389935548143327858L;
@@ -76,15 +82,15 @@ public class OIDC extends Panel {
         super(id);
         setOutputMarkupId(true);
 
-        add(viewModal);
-        viewModal.size(Modal.Size.Extra_large);
-        viewModal.setWindowClosedCallback(target -> viewModal.show(false));
-
         WebMarkupContainer container = new WebMarkupContainer("container");
         add(container.setOutputMarkupId(true));
 
         Mutable<OIDCJWKSTO> oidcjwksto = oidcJWKSRestClient.get();
 
+        add(viewModal);
+        viewModal.size(Modal.Size.Extra_large);
+        viewModal.setWindowClosedCallback(target -> viewModal.show(false));
+
         view = new AjaxLink<>("view") {
 
             private static final long serialVersionUID = 6250423506463465679L;
@@ -124,18 +130,10 @@ public class OIDC extends Panel {
 
             @Override
             public void onClick(final AjaxRequestTarget target) {
-                try {
-                    oidcjwksto.setValue(oidcJWKSRestClient.generate());
-                    generate.setEnabled(false);
-                    view.setEnabled(true);
-
-                    
SyncopeConsoleSession.get().success(getString(Constants.OPERATION_SUCCEEDED));
-                    target.add(container);
-                } catch (Exception e) {
-                    LOG.error("While generating OIDC JWKS", e);
-                    SyncopeConsoleSession.get().onException(e);
-                }
-                ((BaseWebPage) 
pageRef.getPage()).getNotificationPanel().refresh(target);
+                generateModal.header(Model.of("Generate JSON Web Key Sets"));
+                target.add(generateModal.setContent(new 
OIDCJWKSGenerationPanel(
+                        oidcJWKSRestClient, waConfigRestClient, generateModal, 
pageRef)));
+                generateModal.show(true);
             }
 
             @Override
@@ -185,6 +183,17 @@ public class OIDC extends Panel {
         container.add(delete.setOutputMarkupId(true));
         MetaDataRoleAuthorizationStrategy.authorize(delete, ENABLE, 
AMEntitlement.OIDC_JWKS_DELETE);
 
+        generateModal.addSubmitButton();
+        add(generateModal);
+        generateModal.setWindowClosedCallback(target -> {
+            oidcjwksto.setValue(oidcJWKSRestClient.get().get());
+            view.setEnabled(oidcjwksto.get() != null);
+            delete.setEnabled(oidcjwksto.get() != null);
+
+            target.add(container);
+            generateModal.show(false);
+        });
+
         String wellKnownURI = waPrefix + 
"/oidc/.well-known/openid-configuration";
         container.add(new ExternalLink("wellKnownURI", wellKnownURI, 
wellKnownURI));
     }
diff --git 
a/client/am/console/src/main/java/org/apache/syncope/client/console/panels/OIDCJWKSGenerationPanel.java
 
b/client/am/console/src/main/java/org/apache/syncope/client/console/panels/OIDCJWKSGenerationPanel.java
new file mode 100644
index 0000000000..d5d18f96eb
--- /dev/null
+++ 
b/client/am/console/src/main/java/org/apache/syncope/client/console/panels/OIDCJWKSGenerationPanel.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.console.panels;
+
+import java.util.List;
+import org.apache.syncope.client.console.SyncopeConsoleSession;
+import org.apache.syncope.client.console.rest.OIDCJWKSRestClient;
+import org.apache.syncope.client.console.rest.WAConfigRestClient;
+import 
org.apache.syncope.client.console.wicket.ajax.form.IndicatorAjaxEventBehavior;
+import 
org.apache.syncope.client.console.wicket.markup.html.bootstrap.dialog.BaseModal;
+import org.apache.syncope.client.ui.commons.Constants;
+import 
org.apache.syncope.client.ui.commons.markup.html.form.AjaxDropDownChoicePanel;
+import 
org.apache.syncope.client.ui.commons.markup.html.form.AjaxNumberFieldPanel;
+import 
org.apache.syncope.client.ui.commons.markup.html.form.AjaxTextFieldPanel;
+import org.apache.syncope.client.ui.commons.pages.BaseWebPage;
+import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.to.OIDCJWKSTO;
+import org.apache.wicket.PageReference;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.model.Model;
+
+public class OIDCJWKSGenerationPanel extends AbstractModalPanel<OIDCJWKSTO> {
+
+    private static final long serialVersionUID = -3372006007594607067L;
+
+    protected final OIDCJWKSRestClient oidcJWKSRestClient;
+
+    protected final Model<String> jwksKeyIdM;
+
+    protected final Model<String> jwksTypeM;
+
+    protected final Model<Integer> jwksKeySizeM;
+
+    public OIDCJWKSGenerationPanel(
+            final OIDCJWKSRestClient oidcJWKSRestClient,
+            final WAConfigRestClient waConfigRestClient,
+            final BaseModal<OIDCJWKSTO> modal,
+            final PageReference pageRef) {
+
+        super(modal, pageRef);
+        this.oidcJWKSRestClient = oidcJWKSRestClient;
+
+        jwksKeyIdM = Model.of("syncope");
+        try {
+            
jwksKeyIdM.setObject(waConfigRestClient.get("cas.authn.oidc.jwks.core.jwks-key-id").getValues().getFirst());
+        } catch (SyncopeClientException e) {
+            LOG.error("While reading cas.authn.oidc.jwks.core.jwks-key-id", e);
+        }
+        add(new AjaxTextFieldPanel("jwksKeyId", "jwksKeyId", 
jwksKeyIdM).setRequired(true));
+
+        jwksTypeM = Model.of("rsa");
+        try {
+            
jwksTypeM.setObject(waConfigRestClient.get("cas.authn.oidc.jwks.core.jwks-type").getValues().getFirst());
+        } catch (SyncopeClientException e) {
+            LOG.error("While reading cas.authn.oidc.jwks.core.jwks-type", e);
+        }
+        AjaxDropDownChoicePanel<String> jwksType = new 
AjaxDropDownChoicePanel<>("jwksType", "jwksType", jwksTypeM).
+                setChoices(List.of("rsa", "ec"));
+        add(jwksType.setRequired(true));
+
+        jwksKeySizeM = Model.of(2048);
+        try {
+            jwksKeySizeM.setObject(Integer.valueOf(
+                    
waConfigRestClient.get("cas.authn.oidc.jwks.core.jwks-key-size").getValues().getFirst()));
+        } catch (SyncopeClientException e) {
+            LOG.error("While reading cas.authn.oidc.jwks.core.jwks-key-size", 
e);
+        }
+        AjaxNumberFieldPanel<Integer> jwksKeySize = new 
AjaxNumberFieldPanel.Builder<Integer>().step(128).
+                build("jwksKeySize", "jwksKeySize", Integer.class, 
jwksKeySizeM);
+        add(jwksKeySize.setRequired(true));
+
+        jwksType.add(new IndicatorAjaxEventBehavior(Constants.ON_CHANGE) {
+
+            private static final long serialVersionUID = -4255753643957306394L;
+
+            @Override
+            protected void onEvent(final AjaxRequestTarget target) {
+                if ("ec".equals(jwksTypeM.getObject())) {
+                    jwksKeySizeM.setObject(256);
+                } else {
+                    jwksKeySizeM.setObject(2048);
+                }
+                target.add(jwksKeySize);
+            }
+        });
+    }
+
+    @Override
+    public void onSubmit(final AjaxRequestTarget target) {
+        try {
+            oidcJWKSRestClient.generate(jwksKeyIdM.getObject(), 
jwksTypeM.getObject(), jwksKeySizeM.getObject());
+
+            
SyncopeConsoleSession.get().success(getString(Constants.OPERATION_SUCCEEDED));
+            modal.close(target);
+        } catch (Exception e) {
+            LOG.error("While generating OIDC JWKS", e);
+            SyncopeConsoleSession.get().onException(e);
+        }
+        ((BaseWebPage) 
pageRef.getPage()).getNotificationPanel().refresh(target);
+    }
+}
diff --git 
a/client/am/console/src/main/java/org/apache/syncope/client/console/rest/OIDCJWKSRestClient.java
 
b/client/am/console/src/main/java/org/apache/syncope/client/console/rest/OIDCJWKSRestClient.java
index 864cfaace1..70a94e4030 100644
--- 
a/client/am/console/src/main/java/org/apache/syncope/client/console/rest/OIDCJWKSRestClient.java
+++ 
b/client/am/console/src/main/java/org/apache/syncope/client/console/rest/OIDCJWKSRestClient.java
@@ -38,8 +38,8 @@ public class OIDCJWKSRestClient extends BaseRestClient {
         return result;
     }
 
-    public OIDCJWKSTO generate() {
-        Response response = 
getService(OIDCJWKSService.class).generate("syncope", "RSA", 2048);
+    public OIDCJWKSTO generate(final String jwksKeyId, final String jwksType, 
final int jwksKeySize) {
+        Response response = 
getService(OIDCJWKSService.class).generate(jwksKeyId, jwksType, jwksKeySize);
         return response.readEntity(OIDCJWKSTO.class);
     }
 
diff --git 
a/client/am/console/src/main/resources/org/apache/syncope/client/console/panels/OIDC.html
 
b/client/am/console/src/main/resources/org/apache/syncope/client/console/panels/OIDC.html
index 8a4de0f045..f67ee751dc 100644
--- 
a/client/am/console/src/main/resources/org/apache/syncope/client/console/panels/OIDC.html
+++ 
b/client/am/console/src/main/resources/org/apache/syncope/client/console/panels/OIDC.html
@@ -49,6 +49,7 @@ under the License.
       </div>
     </div>
 
+    <div wicket:id="generateModal"/>
     <div wicket:id="viewModal"/>
   </wicket:panel>
 </html>
diff --git 
a/client/am/console/src/main/resources/org/apache/syncope/client/console/panels/OIDCJWKSGenerationPanel.html
 
b/client/am/console/src/main/resources/org/apache/syncope/client/console/panels/OIDCJWKSGenerationPanel.html
new file mode 100644
index 0000000000..cce9c66bf5
--- /dev/null
+++ 
b/client/am/console/src/main/resources/org/apache/syncope/client/console/panels/OIDCJWKSGenerationPanel.html
@@ -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.
+-->
+<html xmlns="http://www.w3.org/1999/xhtml"; >
+  <wicket:extend>
+    <div class="form-group">
+      <span wicket:id="jwksKeyId"/>
+      <span wicket:id="jwksType"/>
+      <span wicket:id="jwksKeySize"/>
+    </div>
+  </wicket:extend>
+</html>
diff --git 
a/core/am/logic/src/main/java/org/apache/syncope/core/logic/AMLogicContext.java 
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/AMLogicContext.java
index bdff58a04e..ff6d338d47 100644
--- 
a/core/am/logic/src/main/java/org/apache/syncope/core/logic/AMLogicContext.java
+++ 
b/core/am/logic/src/main/java/org/apache/syncope/core/logic/AMLogicContext.java
@@ -113,10 +113,11 @@ public class AMLogicContext {
     @Bean
     public OIDCJWKSLogic oidcJWKSLogic(
             final OIDCJWKSDataBinder oidcJWKSDataBinder,
-            final OIDCJWKSDAO dao,
+            final OIDCJWKSDAO oidcJWKSDAO,
+            final WAConfigDAO waConfigDAO,
             final EntityFactory entityFactory) {
 
-        return new OIDCJWKSLogic(oidcJWKSDataBinder, dao, entityFactory);
+        return new OIDCJWKSLogic(oidcJWKSDataBinder, oidcJWKSDAO, waConfigDAO, 
entityFactory);
     }
 
     @ConditionalOnMissingBean
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
index f6bacbafd1..05ec506714 100644
--- 
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
@@ -19,14 +19,17 @@
 package org.apache.syncope.core.logic;
 
 import java.lang.reflect.Method;
+import java.util.List;
 import org.apache.syncope.common.lib.to.OIDCJWKSTO;
 import org.apache.syncope.common.lib.types.AMEntitlement;
 import org.apache.syncope.common.lib.types.IdRepoEntitlement;
 import org.apache.syncope.core.persistence.api.dao.DuplicateException;
 import org.apache.syncope.core.persistence.api.dao.NotFoundException;
 import org.apache.syncope.core.persistence.api.dao.OIDCJWKSDAO;
+import org.apache.syncope.core.persistence.api.dao.WAConfigDAO;
 import org.apache.syncope.core.persistence.api.entity.EntityFactory;
 import org.apache.syncope.core.persistence.api.entity.am.OIDCJWKS;
+import org.apache.syncope.core.persistence.api.entity.am.WAConfigEntry;
 import org.apache.syncope.core.provisioning.api.data.OIDCJWKSDataBinder;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.transaction.annotation.Transactional;
@@ -35,13 +38,21 @@ public class OIDCJWKSLogic extends 
AbstractTransactionalLogic<OIDCJWKSTO> {
 
     protected final OIDCJWKSDataBinder binder;
 
-    protected final OIDCJWKSDAO dao;
+    protected final OIDCJWKSDAO oidcJWKSDAO;
+
+    protected final WAConfigDAO waConfigDAO;
 
     protected final EntityFactory entityFactory;
 
-    public OIDCJWKSLogic(final OIDCJWKSDataBinder binder, final OIDCJWKSDAO 
dao, final EntityFactory entityFactory) {
+    public OIDCJWKSLogic(
+            final OIDCJWKSDataBinder binder,
+            final OIDCJWKSDAO oidcJWKSDAO,
+            final WAConfigDAO waConfigDAO,
+            final EntityFactory entityFactory) {
+
         this.binder = binder;
-        this.dao = dao;
+        this.oidcJWKSDAO = oidcJWKSDAO;
+        this.waConfigDAO = waConfigDAO;
         this.entityFactory = entityFactory;
     }
 
@@ -49,7 +60,7 @@ public class OIDCJWKSLogic extends 
AbstractTransactionalLogic<OIDCJWKSTO> {
             + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
     @Transactional(readOnly = true)
     public OIDCJWKSTO get() {
-        return dao.get().
+        return oidcJWKSDAO.get().
                 map(binder::getOIDCJWKSTO).
                 orElseThrow(() -> new NotFoundException("OIDC JWKS not 
found"));
     }
@@ -57,33 +68,49 @@ public class OIDCJWKSLogic extends 
AbstractTransactionalLogic<OIDCJWKSTO> {
     @PreAuthorize("hasRole('" + AMEntitlement.OIDC_JWKS_GENERATE + "') "
             + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
     public OIDCJWKSTO generate(final String jwksKeyId, final String jwksType, 
final int jwksKeySize) {
-        if (dao.get().isEmpty()) {
-            return binder.getOIDCJWKSTO(dao.save(binder.create(jwksKeyId, 
jwksType, jwksKeySize)));
+        if (oidcJWKSDAO.get().isEmpty()) {
+            OIDCJWKSTO oidcJWKSTO = binder.getOIDCJWKSTO(
+                    oidcJWKSDAO.save(binder.create(jwksKeyId, jwksType, 
jwksKeySize)));
+
+            WAConfigEntry jwksKeyIdConfig = 
entityFactory.newEntity(WAConfigEntry.class);
+            jwksKeyIdConfig.setKey("cas.authn.oidc.jwks.core.jwks-key-id");
+            jwksKeyIdConfig.setValues(List.of(jwksKeyId));
+            waConfigDAO.save(jwksKeyIdConfig);
+
+            WAConfigEntry jwksTypeConfig = 
entityFactory.newEntity(WAConfigEntry.class);
+            jwksTypeConfig.setKey("cas.authn.oidc.jwks.core.jwks-type");
+            jwksTypeConfig.setValues(List.of(jwksType));
+            waConfigDAO.save(jwksTypeConfig);
+
+            WAConfigEntry jwksKeySizeConfig = 
entityFactory.newEntity(WAConfigEntry.class);
+            jwksKeySizeConfig.setKey("cas.authn.oidc.jwks.core.jwks-key-size");
+            jwksKeySizeConfig.setValues(List.of(String.valueOf(jwksKeySize)));
+            waConfigDAO.save(jwksKeySizeConfig);
+
+            return oidcJWKSTO;
         }
+
         throw new DuplicateException("OIDC JWKS already set");
     }
 
     @PreAuthorize("hasRole('" + AMEntitlement.OIDC_JWKS_SET + "') "
             + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
     public OIDCJWKSTO set(final OIDCJWKSTO entityTO) {
-        OIDCJWKS jwks = dao.get().orElse(null);
-        if (jwks == null) {
-            jwks = entityFactory.newEntity(OIDCJWKS.class);
-        }
+        OIDCJWKS jwks = oidcJWKSDAO.get().orElseGet(() -> 
entityFactory.newEntity(OIDCJWKS.class));
         jwks.setJson(entityTO.getJson());
-        return binder.getOIDCJWKSTO(dao.save(jwks));
+        return binder.getOIDCJWKSTO(oidcJWKSDAO.save(jwks));
     }
 
     @PreAuthorize("hasRole('" + AMEntitlement.OIDC_JWKS_DELETE + "')")
     public void delete() {
-        dao.delete();
+        oidcJWKSDAO.delete();
     }
 
     @Override
     protected OIDCJWKSTO resolveReference(final Method method, final Object... 
args)
             throws UnresolvedReferenceException {
 
-        OIDCJWKS jwks = 
dao.get().orElseThrow(UnresolvedReferenceException::new);
+        OIDCJWKS jwks = 
oidcJWKSDAO.get().orElseThrow(UnresolvedReferenceException::new);
         return binder.getOIDCJWKSTO(jwks);
     }
 }
diff --git 
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAOIDCJWKSDAO.java
 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAOIDCJWKSDAO.java
index 3bc38e6bba..87c524fcce 100644
--- 
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAOIDCJWKSDAO.java
+++ 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAOIDCJWKSDAO.java
@@ -43,8 +43,8 @@ public class JPAOIDCJWKSDAO implements OIDCJWKSDAO {
     @Override
     public Optional<OIDCJWKS> get() {
         try {
-            TypedQuery<OIDCJWKS> query = entityManager.
-                    createQuery("SELECT e FROM " + 
JPAOIDCJWKS.class.getSimpleName() + " e", OIDCJWKS.class);
+            TypedQuery<OIDCJWKS> query = entityManager.createQuery(
+                    "SELECT e FROM " + JPAOIDCJWKS.class.getSimpleName() + " 
e", OIDCJWKS.class);
             return Optional.ofNullable(query.getSingleResult());
         } catch (NoResultException e) {
             LOG.debug("No OIDC JWKS found", e);
@@ -59,8 +59,6 @@ public class JPAOIDCJWKSDAO implements OIDCJWKSDAO {
 
     @Override
     public void delete() {
-        entityManager.
-                createQuery("DELETE FROM " + 
JPAOIDCJWKS.class.getSimpleName()).
-                executeUpdate();
+        entityManager.createQuery("DELETE FROM " + 
JPAOIDCJWKS.class.getSimpleName()).executeUpdate();
     }
 }
diff --git 
a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/OIDCJWKSDataBinder.java
 
b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/OIDCJWKSDataBinder.java
index 8baf5da9e0..1e80199f41 100644
--- 
a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/OIDCJWKSDataBinder.java
+++ 
b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/OIDCJWKSDataBinder.java
@@ -23,6 +23,36 @@ import 
org.apache.syncope.core.persistence.api.entity.am.OIDCJWKS;
 
 public interface OIDCJWKSDataBinder {
 
+    String PARAMETER_STATE = "state";
+
+    enum JsonWebKeyLifecycleState {
+        /**
+         * The key state is active and current and is used for crypto 
operations as necessary.
+         * Per the rotation schedule, the key with this status would be 
replaced and rotated by the future key.
+         */
+        CURRENT(0),
+        /**
+         * The key state is one for the future and will take the place of the 
current key per the rotation schedule.
+         */
+        FUTURE(1),
+        /**
+         * Previous key prior to the current key.
+         * This key continues to remain valid and available, and is a 
candidate to be removed from the keystore
+         * per the revocation schedule.
+         */
+        PREVIOUS(2);
+
+        private final long state;
+
+        JsonWebKeyLifecycleState(final long state) {
+            this.state = state;
+        }
+
+        public long getState() {
+            return state;
+        }
+    }
+
     OIDCJWKSTO getOIDCJWKSTO(OIDCJWKS jwks);
 
     OIDCJWKS create(String jwksKeyId, String jwksType, int jwksKeySize);
diff --git a/core/provisioning-java/pom.xml b/core/provisioning-java/pom.xml
index 715318bb2b..4403d45340 100644
--- a/core/provisioning-java/pom.xml
+++ b/core/provisioning-java/pom.xml
@@ -54,6 +54,11 @@ under the License.
       <artifactId>spring-retry</artifactId>
     </dependency>
 
+    <dependency>
+      <groupId>org.bitbucket.b_c</groupId>
+      <artifactId>jose4j</artifactId>
+    </dependency>
+
     <dependency>
       <groupId>com.fasterxml.jackson.dataformat</groupId>
       <artifactId>jackson-dataformat-csv</artifactId>
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
index c8296be68c..862c83decd 100644
--- 
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
@@ -18,20 +18,9 @@
  */
 package org.apache.syncope.core.provisioning.java.data;
 
-import com.nimbusds.jose.JOSEException;
-import com.nimbusds.jose.jwk.Curve;
-import com.nimbusds.jose.jwk.ECKey;
-import com.nimbusds.jose.jwk.JWK;
-import com.nimbusds.jose.jwk.JWKSet;
-import com.nimbusds.jose.jwk.KeyUse;
-import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
-import com.nimbusds.jose.util.JSONObjectUtils;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.KeyPair;
-import java.security.KeyPairGenerator;
-import java.security.NoSuchAlgorithmException;
-import java.security.interfaces.ECPrivateKey;
-import java.security.interfaces.ECPublicKey;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
 import org.apache.syncope.common.lib.SyncopeClientException;
 import org.apache.syncope.common.lib.to.OIDCJWKSTO;
 import org.apache.syncope.common.lib.types.ClientExceptionType;
@@ -39,6 +28,15 @@ import 
org.apache.syncope.core.persistence.api.entity.EntityFactory;
 import org.apache.syncope.core.persistence.api.entity.am.OIDCJWKS;
 import org.apache.syncope.core.provisioning.api.data.OIDCJWKSDataBinder;
 import org.apache.syncope.core.spring.security.SecureRandomUtils;
+import org.jose4j.jwk.EcJwkGenerator;
+import org.jose4j.jwk.JsonWebKey;
+import org.jose4j.jwk.JsonWebKeySet;
+import org.jose4j.jwk.PublicJsonWebKey;
+import org.jose4j.jwk.RsaJwkGenerator;
+import org.jose4j.jwk.Use;
+import org.jose4j.jws.AlgorithmIdentifiers;
+import org.jose4j.keys.EllipticCurves;
+import org.jose4j.lang.JoseException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -54,61 +52,59 @@ public class OIDCJWKSDataBinderImpl implements 
OIDCJWKSDataBinder {
 
     @Override
     public OIDCJWKSTO getOIDCJWKSTO(final OIDCJWKS jwks) {
-        return new 
OIDCJWKSTO.Builder().json(jwks.getJson()).key(jwks.getKey()).build();
+        return new OIDCJWKSTO.Builder().
+                key(jwks.getKey()).
+                json(jwks.getJson()).
+                build();
     }
 
-    @Override
-    public OIDCJWKS create(final String jwksKeyId, final String jwksType, 
final int jwksKeySize) {
-        JWK jwk;
-        try {
-            switch (jwksType.trim().toLowerCase()) {
-                case "ec":
-                    KeyPairGenerator gen = KeyPairGenerator.getInstance("EC");
-                    KeyPair keyPair;
-                    switch (jwksKeySize) {
-                        case 384:
-                            gen.initialize(Curve.P_384.toECParameterSpec());
-                            keyPair = gen.generateKeyPair();
-                            jwk = new ECKey.Builder(Curve.P_384, (ECPublicKey) 
keyPair.getPublic()).
-                                    privateKey((ECPrivateKey) 
keyPair.getPrivate()).
-                                    keyUse(KeyUse.SIGNATURE).
-                                    keyID(jwksKeyId.concat("-").
-                                            
concat(SecureRandomUtils.generateRandomUUID().toString().substring(0, 8))).
-                                    build();
-                            break;
+    protected PublicJsonWebKey generate(
+            final String jwksKeyId,
+            final String jwksType,
+            final int jwksKeySize,
+            final String use,
+            final JsonWebKeyLifecycleState state) throws JoseException {
 
-                        case 512:
-                            gen.initialize(Curve.P_521.toECParameterSpec());
-                            keyPair = gen.generateKeyPair();
-                            jwk = new ECKey.Builder(Curve.P_521, (ECPublicKey) 
keyPair.getPublic()).
-                                    privateKey((ECPrivateKey) 
keyPair.getPrivate()).
-                                    keyUse(KeyUse.SIGNATURE).
-                                    keyID(jwksKeyId.concat("-").
-                                            
concat(SecureRandomUtils.generateRandomUUID().toString().substring(0, 8))).
-                                    build();
-                            break;
+        PublicJsonWebKey jwk;
+        switch (jwksType.trim().toLowerCase(Locale.ENGLISH)) {
+            case "ec":
+                switch (jwksKeySize) {
+                    case 384:
+                        jwk = EcJwkGenerator.generateJwk(EllipticCurves.P384);
+                        
jwk.setAlgorithm(AlgorithmIdentifiers.ECDSA_USING_P384_CURVE_AND_SHA384);
+                        break;
 
-                        default:
-                            gen.initialize(Curve.P_256.toECParameterSpec());
-                            keyPair = gen.generateKeyPair();
-                            jwk = new ECKey.Builder(Curve.P_256, (ECPublicKey) 
keyPair.getPublic()).
-                                    privateKey((ECPrivateKey) 
keyPair.getPrivate()).
-                                    keyUse(KeyUse.SIGNATURE).
-                                    keyID(jwksKeyId.concat("-").
-                                            
concat(SecureRandomUtils.generateRandomUUID().toString().substring(0, 8))).
-                                    build();
-                    }
-                    break;
+                    case 512:
+                        jwk = EcJwkGenerator.generateJwk(EllipticCurves.P521);
+                        
jwk.setAlgorithm(AlgorithmIdentifiers.ECDSA_USING_P521_CURVE_AND_SHA512);
+                        break;
+
+                    default:
+                        jwk = EcJwkGenerator.generateJwk(EllipticCurves.P256);
+                        
jwk.setAlgorithm(AlgorithmIdentifiers.ECDSA_USING_P521_CURVE_AND_SHA512);
+                }
+                break;
+
+            case "rsa":
+            default:
+                jwk = RsaJwkGenerator.generateJwk(jwksKeySize);
+        }
 
-                case "rsa":
-                default:
-                    jwk = new RSAKeyGenerator(jwksKeySize).
-                            keyUse(KeyUse.SIGNATURE).
-                            keyID(jwksKeyId.concat("-").
-                                    
concat(SecureRandomUtils.generateRandomUUID().toString().substring(0, 8))).
-                            generate();
-            }
-        } catch (JOSEException | InvalidAlgorithmParameterException | 
NoSuchAlgorithmException e) {
+        
jwk.setKeyId(jwksKeyId.concat("-").concat(SecureRandomUtils.generateRandomLetters(8)));
+        jwk.setUse(use);
+        jwk.setOtherParameter(PARAMETER_STATE, state.getState());
+        return jwk;
+    }
+
+    @Override
+    public OIDCJWKS create(final String jwksKeyId, final String jwksType, 
final int jwksKeySize) {
+        List<PublicJsonWebKey> keys = new ArrayList<>();
+        try {
+            keys.add(generate(jwksKeyId, jwksType, jwksKeySize, Use.SIGNATURE, 
JsonWebKeyLifecycleState.CURRENT));
+            keys.add(generate(jwksKeyId, jwksType, jwksKeySize, 
Use.ENCRYPTION, JsonWebKeyLifecycleState.CURRENT));
+            keys.add(generate(jwksKeyId, jwksType, jwksKeySize, Use.SIGNATURE, 
JsonWebKeyLifecycleState.FUTURE));
+            keys.add(generate(jwksKeyId, jwksType, jwksKeySize, 
Use.ENCRYPTION, JsonWebKeyLifecycleState.FUTURE));
+        } catch (JoseException e) {
             LOG.error("Could not create OIDC JWKS", e);
 
             SyncopeClientException sce = 
SyncopeClientException.build(ClientExceptionType.Unknown);
@@ -116,8 +112,8 @@ public class OIDCJWKSDataBinderImpl implements 
OIDCJWKSDataBinder {
             throw sce;
         }
 
-        OIDCJWKS jwks = entityFactory.newEntity(OIDCJWKS.class);
-        jwks.setJson(JSONObjectUtils.toJSONString(new 
JWKSet(jwk).toJSONObject(false)));
-        return jwks;
+        OIDCJWKS oidcJWKS = entityFactory.newEntity(OIDCJWKS.class);
+        oidcJWKS.setJson(new 
JsonWebKeySet(keys).toJson(JsonWebKey.OutputControlLevel.INCLUDE_PRIVATE));
+        return oidcJWKS;
     }
 }
diff --git a/docker/core/LICENSE b/docker/core/LICENSE
index 945dac0658..e3dd337f65 100644
--- a/docker/core/LICENSE
+++ b/docker/core/LICENSE
@@ -1324,3 +1324,7 @@ This is licensed under the AL 2.0, see above.
 
 For SnakeYAML (http://www.snakeyaml.org/):
 This is licensed under the AL 2.0, see above.
+
+==
+For jose.4.j (https://bitbucket.org/b_c/jose4j/):
+This is licensed under the AL 2.0, see above.
diff --git a/pom.xml b/pom.xml
index 838e0dd9df..8952dac2c6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1191,6 +1191,12 @@ under the License.
         <version>${disruptor.version}</version>
       </dependency>
 
+      <dependency>
+        <groupId>org.bitbucket.b_c</groupId>
+        <artifactId>jose4j</artifactId>
+        <version>0.9.6</version>
+      </dependency>
+
       <dependency>
         <groupId>org.apache.pdfbox</groupId>
         <artifactId>pdfbox</artifactId>
diff --git 
a/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/WAContext.java 
b/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/WAContext.java
index 7ce97dcf7e..3c7e6b6088 100644
--- 
a/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/WAContext.java
+++ 
b/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/WAContext.java
@@ -373,13 +373,15 @@ public class WAContext {
     @Bean
     public OidcJsonWebKeystoreGeneratorService 
oidcJsonWebKeystoreGeneratorService(
             final CasConfigurationProperties casProperties,
-            final WARestClient waRestClient) {
+            final WARestClient waRestClient,
+            final ApplicationContext applicationContext) {
 
         return new WAOIDCJWKSGeneratorService(
                 waRestClient,
                 
casProperties.getAuthn().getOidc().getJwks().getCore().getJwksKeyId(),
                 
casProperties.getAuthn().getOidc().getJwks().getCore().getJwksType(),
-                
casProperties.getAuthn().getOidc().getJwks().getCore().getJwksKeySize());
+                
casProperties.getAuthn().getOidc().getJwks().getCore().getJwksKeySize(),
+                applicationContext);
     }
 
     @Bean
diff --git 
a/wa/starter/src/main/java/org/apache/syncope/wa/starter/oidc/WAOIDCJWKSGeneratorService.java
 
b/wa/starter/src/main/java/org/apache/syncope/wa/starter/oidc/WAOIDCJWKSGeneratorService.java
index 5ba8bcf5ed..23d8bfd9cb 100644
--- 
a/wa/starter/src/main/java/org/apache/syncope/wa/starter/oidc/WAOIDCJWKSGeneratorService.java
+++ 
b/wa/starter/src/main/java/org/apache/syncope/wa/starter/oidc/WAOIDCJWKSGeneratorService.java
@@ -26,11 +26,15 @@ import org.apache.syncope.common.lib.to.OIDCJWKSTO;
 import org.apache.syncope.common.lib.types.ClientExceptionType;
 import org.apache.syncope.common.rest.api.service.OIDCJWKSService;
 import org.apache.syncope.wa.bootstrap.WARestClient;
+import org.apereo.cas.oidc.jwks.generator.OidcJsonWebKeystoreGeneratedEvent;
 import org.apereo.cas.oidc.jwks.generator.OidcJsonWebKeystoreGeneratorService;
+import org.apereo.inspektr.common.web.ClientInfo;
+import org.apereo.inspektr.common.web.ClientInfoHolder;
 import org.jose4j.jwk.JsonWebKey;
 import org.jose4j.jwk.JsonWebKeySet;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.context.ApplicationContext;
 import org.springframework.core.io.ByteArrayResource;
 import org.springframework.core.io.Resource;
 
@@ -46,16 +50,20 @@ public class WAOIDCJWKSGeneratorService implements 
OidcJsonWebKeystoreGeneratorS
 
     protected final int jwksKeySize;
 
+    protected final ApplicationContext applicationContext;
+
     public WAOIDCJWKSGeneratorService(
             final WARestClient waRestClient,
             final String jwksKeyId,
             final String jwksType,
-            final int jwksKeySize) {
+            final int jwksKeySize,
+            final ApplicationContext applicationContext) {
 
         this.waRestClient = waRestClient;
         this.jwksKeyId = jwksKeyId;
         this.jwksType = jwksType;
         this.jwksKeySize = jwksKeySize;
+        this.applicationContext = applicationContext;
     }
 
     @Override
@@ -92,6 +100,10 @@ public class WAOIDCJWKSGeneratorService implements 
OidcJsonWebKeystoreGeneratorS
         if (jwksTO == null) {
             throw new IllegalStateException("Unable to determine OIDC JWKS 
resource");
         }
-        return new 
ByteArrayResource(jwksTO.getJson().getBytes(StandardCharsets.UTF_8), "OIDC 
JWKS");
+
+        Resource result = new 
ByteArrayResource(jwksTO.getJson().getBytes(StandardCharsets.UTF_8), "OIDC 
JWKS");
+        ClientInfo clientInfo = ClientInfoHolder.getClientInfo();
+        applicationContext.publishEvent(new 
OidcJsonWebKeystoreGeneratedEvent(this, result, clientInfo));
+        return result;
     }
 }


Reply via email to