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;
}
}