This is an automated email from the ASF dual-hosted git repository. ilgrosso pushed a commit to branch 3_0_X in repository https://gitbox.apache.org/repos/asf/syncope.git
commit bd339560cb2506a07f9b0c8b7a770269f8fd472d 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 | 8 +- .../apache/syncope/core/logic/OIDCJWKSLogic.java | 67 ++++++++--- .../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 | 7 +- .../starter/oidc/WAOIDCJWKSGeneratorService.java | 13 ++- 15 files changed, 354 insertions(+), 114 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 4bc40ab029..e439c26317 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 @@ -24,6 +24,7 @@ import java.io.IOException; import java.util.concurrent.atomic.AtomicReference; 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; @@ -54,6 +55,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; @@ -75,15 +81,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)); AtomicReference<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; @@ -123,18 +129,10 @@ public class OIDC extends Panel { @Override public void onClick(final AjaxRequestTarget target) { - try { - oidcjwksto.set(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 @@ -184,6 +182,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.set(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..ae57f86be6 --- /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().get(0)); + } 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().get(0)); + } 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().get(0))); + } 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 8db5f3ab6d..a7abf44c8b 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 @@ -37,8 +37,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 e95ce63322..0e34db86e3 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 8a365cc522..07346e6f58 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 @@ -114,10 +114,12 @@ public class AMLogicContext { @ConditionalOnMissingBean @Bean public OIDCJWKSLogic oidcJWKSLogic( - final OIDCJWKSDataBinder binder, - final OIDCJWKSDAO dao) { + final OIDCJWKSDataBinder oidcJWKSDataBinder, + final OIDCJWKSDAO oidcJWKSDAO, + final WAConfigDAO waConfigDAO, + final EntityFactory entityFactory) { - return new OIDCJWKSLogic(binder, dao); + 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 e088d7bbd3..d998beae54 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 @@ -20,13 +20,17 @@ package org.apache.syncope.core.logic; import java.lang.reflect.Method; import java.util.Optional; +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,52 +39,81 @@ 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 oidcJWKSDAO, + final WAConfigDAO waConfigDAO, + final EntityFactory entityFactory) { - public OIDCJWKSLogic(final OIDCJWKSDataBinder binder, final OIDCJWKSDAO dao) { this.binder = binder; - this.dao = dao; + this.oidcJWKSDAO = oidcJWKSDAO; + this.waConfigDAO = waConfigDAO; + this.entityFactory = entityFactory; } @PreAuthorize("hasRole('" + AMEntitlement.OIDC_JWKS_READ + "') " + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')") @Transactional(readOnly = true) public OIDCJWKSTO get() { - return Optional.ofNullable(dao.get()). + return Optional.ofNullable(oidcJWKSDAO.get()). map(binder::getOIDCJWKSTO). orElseThrow(() -> new NotFoundException("OIDC JWKS not found")); } + @PreAuthorize("hasRole('" + AMEntitlement.OIDC_JWKS_SET + "') " + + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')") + public OIDCJWKSTO set(final OIDCJWKSTO entityTO) { + OIDCJWKS jwks = oidcJWKSDAO.get(); + jwks.setJson(entityTO.getJson()); + return binder.getOIDCJWKSTO(oidcJWKSDAO.save(jwks)); + } + @PreAuthorize("hasRole('" + AMEntitlement.OIDC_JWKS_GENERATE + "') " + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')") public OIDCJWKSTO generate(final String jwksKeyId, final String jwksType, final int jwksKeySize) { - OIDCJWKS jwks = dao.get(); - if (jwks == null) { - return binder.getOIDCJWKSTO(dao.save(binder.create(jwksKeyId, jwksType, jwksKeySize))); + if (oidcJWKSDAO.get() == null) { + 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_DELETE + "')") public void delete() { - dao.delete(); + oidcJWKSDAO.delete(); } @Override protected OIDCJWKSTO resolveReference(final Method method, final Object... args) throws UnresolvedReferenceException { - OIDCJWKS jwks = dao.get(); + OIDCJWKS jwks = oidcJWKSDAO.get(); if (jwks == null) { throw new UnresolvedReferenceException(); } return binder.getOIDCJWKSTO(jwks); } - - @PreAuthorize("hasRole('" + AMEntitlement.OIDC_JWKS_SET + "') " - + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')") - public OIDCJWKSTO set(final OIDCJWKSTO entityTO) { - OIDCJWKS jwks = dao.get(); - jwks.setJson(entityTO.getJson()); - return binder.getOIDCJWKSTO(dao.save(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 3e1cd75ee5..2edb514f67 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 @@ -31,8 +31,8 @@ public class JPAOIDCJWKSDAO extends AbstractDAO<OIDCJWKS> implements OIDCJWKSDAO @Override public 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 query.getSingleResult(); } catch (final NoResultException e) { LOG.debug(e.getMessage()); @@ -47,8 +47,6 @@ public class JPAOIDCJWKSDAO extends AbstractDAO<OIDCJWKS> 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 4e14d40fd0..0c724d9d5a 100644 --- a/core/provisioning-java/pom.xml +++ b/core/provisioning-java/pom.xml @@ -63,6 +63,11 @@ under the License. <artifactId>geronimo-javamail_1.4_mail</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 6d64d92476..733c2e0f64 100644 --- a/docker/core/LICENSE +++ b/docker/core/LICENSE @@ -1319,3 +1319,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 ef3a22fce6..bce293734f 100644 --- a/pom.xml +++ b/pom.xml @@ -1298,6 +1298,12 @@ under the License. <version>${slf4j.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 0fec1e6d11..3a66274c86 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 @@ -105,6 +105,7 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -371,13 +372,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 04e36a99ec..053e37d541 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,13 @@ 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.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 +48,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 @@ -93,6 +99,9 @@ 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"); + applicationContext.publishEvent(new OidcJsonWebKeystoreGeneratedEvent(this, result)); + return result; } }
