Skip to content

Commit 7ed381f

Browse files
authored
[SYNCOPE-1936] Generate OIDC JWKS as CAS does (#1252)
1 parent 7a5c697 commit 7ed381f

File tree

15 files changed

+344
-109
lines changed

15 files changed

+344
-109
lines changed

client/am/console/src/main/java/org/apache/syncope/client/console/panels/OIDC.java

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.apache.commons.lang3.mutable.Mutable;
2626
import org.apache.syncope.client.console.SyncopeConsoleSession;
2727
import org.apache.syncope.client.console.rest.OIDCJWKSRestClient;
28+
import org.apache.syncope.client.console.rest.WAConfigRestClient;
2829
import org.apache.syncope.client.console.wicket.markup.html.bootstrap.dialog.BaseModal;
2930
import org.apache.syncope.client.console.wicket.markup.html.form.JsonEditorPanel;
3031
import org.apache.syncope.client.ui.commons.Constants;
@@ -55,6 +56,11 @@ public class OIDC extends Panel {
5556
@SpringBean
5657
protected OIDCJWKSRestClient oidcJWKSRestClient;
5758

59+
@SpringBean
60+
protected WAConfigRestClient waConfigRestClient;
61+
62+
protected final BaseModal<OIDCJWKSTO> generateModal = new BaseModal<>("generateModal");
63+
5864
protected final BaseModal<String> viewModal = new BaseModal<>("viewModal") {
5965

6066
private static final long serialVersionUID = 389935548143327858L;
@@ -76,15 +82,15 @@ public OIDC(final String id, final String waPrefix, final PageReference pageRef)
7682
super(id);
7783
setOutputMarkupId(true);
7884

79-
add(viewModal);
80-
viewModal.size(Modal.Size.Extra_large);
81-
viewModal.setWindowClosedCallback(target -> viewModal.show(false));
82-
8385
WebMarkupContainer container = new WebMarkupContainer("container");
8486
add(container.setOutputMarkupId(true));
8587

8688
Mutable<OIDCJWKSTO> oidcjwksto = oidcJWKSRestClient.get();
8789

90+
add(viewModal);
91+
viewModal.size(Modal.Size.Extra_large);
92+
viewModal.setWindowClosedCallback(target -> viewModal.show(false));
93+
8894
view = new AjaxLink<>("view") {
8995

9096
private static final long serialVersionUID = 6250423506463465679L;
@@ -124,18 +130,10 @@ protected void onComponentTag(final ComponentTag tag) {
124130

125131
@Override
126132
public void onClick(final AjaxRequestTarget target) {
127-
try {
128-
oidcjwksto.setValue(oidcJWKSRestClient.generate());
129-
generate.setEnabled(false);
130-
view.setEnabled(true);
131-
132-
SyncopeConsoleSession.get().success(getString(Constants.OPERATION_SUCCEEDED));
133-
target.add(container);
134-
} catch (Exception e) {
135-
LOG.error("While generating OIDC JWKS", e);
136-
SyncopeConsoleSession.get().onException(e);
137-
}
138-
((BaseWebPage) pageRef.getPage()).getNotificationPanel().refresh(target);
133+
generateModal.header(Model.of("Generate JSON Web Key Sets"));
134+
target.add(generateModal.setContent(new OIDCJWKSGenerationPanel(
135+
oidcJWKSRestClient, waConfigRestClient, generateModal, pageRef)));
136+
generateModal.show(true);
139137
}
140138

141139
@Override
@@ -185,6 +183,17 @@ protected void onComponentTag(final ComponentTag tag) {
185183
container.add(delete.setOutputMarkupId(true));
186184
MetaDataRoleAuthorizationStrategy.authorize(delete, ENABLE, AMEntitlement.OIDC_JWKS_DELETE);
187185

186+
generateModal.addSubmitButton();
187+
add(generateModal);
188+
generateModal.setWindowClosedCallback(target -> {
189+
oidcjwksto.setValue(oidcJWKSRestClient.get().get());
190+
view.setEnabled(oidcjwksto.get() != null);
191+
delete.setEnabled(oidcjwksto.get() != null);
192+
193+
target.add(container);
194+
generateModal.show(false);
195+
});
196+
188197
String wellKnownURI = waPrefix + "/oidc/.well-known/openid-configuration";
189198
container.add(new ExternalLink("wellKnownURI", wellKnownURI, wellKnownURI));
190199
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.syncope.client.console.panels;
20+
21+
import java.util.List;
22+
import org.apache.syncope.client.console.SyncopeConsoleSession;
23+
import org.apache.syncope.client.console.rest.OIDCJWKSRestClient;
24+
import org.apache.syncope.client.console.rest.WAConfigRestClient;
25+
import org.apache.syncope.client.console.wicket.ajax.form.IndicatorAjaxEventBehavior;
26+
import org.apache.syncope.client.console.wicket.markup.html.bootstrap.dialog.BaseModal;
27+
import org.apache.syncope.client.ui.commons.Constants;
28+
import org.apache.syncope.client.ui.commons.markup.html.form.AjaxDropDownChoicePanel;
29+
import org.apache.syncope.client.ui.commons.markup.html.form.AjaxNumberFieldPanel;
30+
import org.apache.syncope.client.ui.commons.markup.html.form.AjaxTextFieldPanel;
31+
import org.apache.syncope.client.ui.commons.pages.BaseWebPage;
32+
import org.apache.syncope.common.lib.SyncopeClientException;
33+
import org.apache.syncope.common.lib.to.OIDCJWKSTO;
34+
import org.apache.wicket.PageReference;
35+
import org.apache.wicket.ajax.AjaxRequestTarget;
36+
import org.apache.wicket.model.Model;
37+
38+
public class OIDCJWKSGenerationPanel extends AbstractModalPanel<OIDCJWKSTO> {
39+
40+
private static final long serialVersionUID = -3372006007594607067L;
41+
42+
protected final OIDCJWKSRestClient oidcJWKSRestClient;
43+
44+
protected final Model<String> jwksKeyIdM;
45+
46+
protected final Model<String> jwksTypeM;
47+
48+
protected final Model<Integer> jwksKeySizeM;
49+
50+
public OIDCJWKSGenerationPanel(
51+
final OIDCJWKSRestClient oidcJWKSRestClient,
52+
final WAConfigRestClient waConfigRestClient,
53+
final BaseModal<OIDCJWKSTO> modal,
54+
final PageReference pageRef) {
55+
56+
super(modal, pageRef);
57+
this.oidcJWKSRestClient = oidcJWKSRestClient;
58+
59+
jwksKeyIdM = Model.of("syncope");
60+
try {
61+
jwksKeyIdM.setObject(waConfigRestClient.get("cas.authn.oidc.jwks.core.jwks-key-id").getValues().getFirst());
62+
} catch (SyncopeClientException e) {
63+
LOG.error("While reading cas.authn.oidc.jwks.core.jwks-key-id", e);
64+
}
65+
add(new AjaxTextFieldPanel("jwksKeyId", "jwksKeyId", jwksKeyIdM).setRequired(true));
66+
67+
jwksTypeM = Model.of("rsa");
68+
try {
69+
jwksTypeM.setObject(waConfigRestClient.get("cas.authn.oidc.jwks.core.jwks-type").getValues().getFirst());
70+
} catch (SyncopeClientException e) {
71+
LOG.error("While reading cas.authn.oidc.jwks.core.jwks-type", e);
72+
}
73+
AjaxDropDownChoicePanel<String> jwksType = new AjaxDropDownChoicePanel<>("jwksType", "jwksType", jwksTypeM).
74+
setChoices(List.of("rsa", "ec"));
75+
add(jwksType.setRequired(true));
76+
77+
jwksKeySizeM = Model.of(2048);
78+
try {
79+
jwksKeySizeM.setObject(Integer.valueOf(
80+
waConfigRestClient.get("cas.authn.oidc.jwks.core.jwks-key-size").getValues().getFirst()));
81+
} catch (SyncopeClientException e) {
82+
LOG.error("While reading cas.authn.oidc.jwks.core.jwks-key-size", e);
83+
}
84+
AjaxNumberFieldPanel<Integer> jwksKeySize = new AjaxNumberFieldPanel.Builder<Integer>().step(128).
85+
build("jwksKeySize", "jwksKeySize", Integer.class, jwksKeySizeM);
86+
add(jwksKeySize.setRequired(true));
87+
88+
jwksType.add(new IndicatorAjaxEventBehavior(Constants.ON_CHANGE) {
89+
90+
private static final long serialVersionUID = -4255753643957306394L;
91+
92+
@Override
93+
protected void onEvent(final AjaxRequestTarget target) {
94+
if ("ec".equals(jwksTypeM.getObject())) {
95+
jwksKeySizeM.setObject(256);
96+
} else {
97+
jwksKeySizeM.setObject(2048);
98+
}
99+
target.add(jwksKeySize);
100+
}
101+
});
102+
}
103+
104+
@Override
105+
public void onSubmit(final AjaxRequestTarget target) {
106+
try {
107+
oidcJWKSRestClient.generate(jwksKeyIdM.getObject(), jwksTypeM.getObject(), jwksKeySizeM.getObject());
108+
109+
SyncopeConsoleSession.get().success(getString(Constants.OPERATION_SUCCEEDED));
110+
modal.close(target);
111+
} catch (Exception e) {
112+
LOG.error("While generating OIDC JWKS", e);
113+
SyncopeConsoleSession.get().onException(e);
114+
}
115+
((BaseWebPage) pageRef.getPage()).getNotificationPanel().refresh(target);
116+
}
117+
}

client/am/console/src/main/java/org/apache/syncope/client/console/rest/OIDCJWKSRestClient.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ public Mutable<OIDCJWKSTO> get() {
3838
return result;
3939
}
4040

41-
public OIDCJWKSTO generate() {
42-
Response response = getService(OIDCJWKSService.class).generate("syncope", "RSA", 2048);
41+
public OIDCJWKSTO generate(final String jwksKeyId, final String jwksType, final int jwksKeySize) {
42+
Response response = getService(OIDCJWKSService.class).generate(jwksKeyId, jwksType, jwksKeySize);
4343
return response.readEntity(OIDCJWKSTO.class);
4444
}
4545

client/am/console/src/main/resources/org/apache/syncope/client/console/panels/OIDC.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ <h3 class="card-title">Well-Known URI</h3>
4949
</div>
5050
</div>
5151

52+
<div wicket:id="generateModal"/>
5253
<div wicket:id="viewModal"/>
5354
</wicket:panel>
5455
</html>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<!--
2+
Licensed to the Apache Software Foundation (ASF) under one
3+
or more contributor license agreements. See the NOTICE file
4+
distributed with this work for additional information
5+
regarding copyright ownership. The ASF licenses this file
6+
to you under the Apache License, Version 2.0 (the
7+
"License"); you may not use this file except in compliance
8+
with the License. You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing,
13+
software distributed under the License is distributed on an
14+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
KIND, either express or implied. See the License for the
16+
specific language governing permissions and limitations
17+
under the License.
18+
-->
19+
<html xmlns="http://www.w3.org/1999/xhtml" >
20+
<wicket:extend>
21+
<div class="form-group">
22+
<span wicket:id="jwksKeyId"/>
23+
<span wicket:id="jwksType"/>
24+
<span wicket:id="jwksKeySize"/>
25+
</div>
26+
</wicket:extend>
27+
</html>

core/am/logic/src/main/java/org/apache/syncope/core/logic/AMLogicContext.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,11 @@ public ClientAppLogic clientAppLogic(
113113
@Bean
114114
public OIDCJWKSLogic oidcJWKSLogic(
115115
final OIDCJWKSDataBinder oidcJWKSDataBinder,
116-
final OIDCJWKSDAO dao,
116+
final OIDCJWKSDAO oidcJWKSDAO,
117+
final WAConfigDAO waConfigDAO,
117118
final EntityFactory entityFactory) {
118119

119-
return new OIDCJWKSLogic(oidcJWKSDataBinder, dao, entityFactory);
120+
return new OIDCJWKSLogic(oidcJWKSDataBinder, oidcJWKSDAO, waConfigDAO, entityFactory);
120121
}
121122

122123
@ConditionalOnMissingBean

core/am/logic/src/main/java/org/apache/syncope/core/logic/OIDCJWKSLogic.java

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,17 @@
1919
package org.apache.syncope.core.logic;
2020

2121
import java.lang.reflect.Method;
22+
import java.util.List;
2223
import org.apache.syncope.common.lib.to.OIDCJWKSTO;
2324
import org.apache.syncope.common.lib.types.AMEntitlement;
2425
import org.apache.syncope.common.lib.types.IdRepoEntitlement;
2526
import org.apache.syncope.core.persistence.api.dao.DuplicateException;
2627
import org.apache.syncope.core.persistence.api.dao.NotFoundException;
2728
import org.apache.syncope.core.persistence.api.dao.OIDCJWKSDAO;
29+
import org.apache.syncope.core.persistence.api.dao.WAConfigDAO;
2830
import org.apache.syncope.core.persistence.api.entity.EntityFactory;
2931
import org.apache.syncope.core.persistence.api.entity.am.OIDCJWKS;
32+
import org.apache.syncope.core.persistence.api.entity.am.WAConfigEntry;
3033
import org.apache.syncope.core.provisioning.api.data.OIDCJWKSDataBinder;
3134
import org.springframework.security.access.prepost.PreAuthorize;
3235
import org.springframework.transaction.annotation.Transactional;
@@ -35,55 +38,79 @@ public class OIDCJWKSLogic extends AbstractTransactionalLogic<OIDCJWKSTO> {
3538

3639
protected final OIDCJWKSDataBinder binder;
3740

38-
protected final OIDCJWKSDAO dao;
41+
protected final OIDCJWKSDAO oidcJWKSDAO;
42+
43+
protected final WAConfigDAO waConfigDAO;
3944

4045
protected final EntityFactory entityFactory;
4146

42-
public OIDCJWKSLogic(final OIDCJWKSDataBinder binder, final OIDCJWKSDAO dao, final EntityFactory entityFactory) {
47+
public OIDCJWKSLogic(
48+
final OIDCJWKSDataBinder binder,
49+
final OIDCJWKSDAO oidcJWKSDAO,
50+
final WAConfigDAO waConfigDAO,
51+
final EntityFactory entityFactory) {
52+
4353
this.binder = binder;
44-
this.dao = dao;
54+
this.oidcJWKSDAO = oidcJWKSDAO;
55+
this.waConfigDAO = waConfigDAO;
4556
this.entityFactory = entityFactory;
4657
}
4758

4859
@PreAuthorize("hasRole('" + AMEntitlement.OIDC_JWKS_READ + "') "
4960
+ "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
5061
@Transactional(readOnly = true)
5162
public OIDCJWKSTO get() {
52-
return dao.get().
63+
return oidcJWKSDAO.get().
5364
map(binder::getOIDCJWKSTO).
5465
orElseThrow(() -> new NotFoundException("OIDC JWKS not found"));
5566
}
5667

5768
@PreAuthorize("hasRole('" + AMEntitlement.OIDC_JWKS_GENERATE + "') "
5869
+ "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
5970
public OIDCJWKSTO generate(final String jwksKeyId, final String jwksType, final int jwksKeySize) {
60-
if (dao.get().isEmpty()) {
61-
return binder.getOIDCJWKSTO(dao.save(binder.create(jwksKeyId, jwksType, jwksKeySize)));
71+
if (oidcJWKSDAO.get().isEmpty()) {
72+
OIDCJWKSTO oidcJWKSTO = binder.getOIDCJWKSTO(
73+
oidcJWKSDAO.save(binder.create(jwksKeyId, jwksType, jwksKeySize)));
74+
75+
WAConfigEntry jwksKeyIdConfig = entityFactory.newEntity(WAConfigEntry.class);
76+
jwksKeyIdConfig.setKey("cas.authn.oidc.jwks.core.jwks-key-id");
77+
jwksKeyIdConfig.setValues(List.of(jwksKeyId));
78+
waConfigDAO.save(jwksKeyIdConfig);
79+
80+
WAConfigEntry jwksTypeConfig = entityFactory.newEntity(WAConfigEntry.class);
81+
jwksTypeConfig.setKey("cas.authn.oidc.jwks.core.jwks-type");
82+
jwksTypeConfig.setValues(List.of(jwksType));
83+
waConfigDAO.save(jwksTypeConfig);
84+
85+
WAConfigEntry jwksKeySizeConfig = entityFactory.newEntity(WAConfigEntry.class);
86+
jwksKeySizeConfig.setKey("cas.authn.oidc.jwks.core.jwks-key-size");
87+
jwksKeySizeConfig.setValues(List.of(String.valueOf(jwksKeySize)));
88+
waConfigDAO.save(jwksKeySizeConfig);
89+
90+
return oidcJWKSTO;
6291
}
92+
6393
throw new DuplicateException("OIDC JWKS already set");
6494
}
6595

6696
@PreAuthorize("hasRole('" + AMEntitlement.OIDC_JWKS_SET + "') "
6797
+ "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
6898
public OIDCJWKSTO set(final OIDCJWKSTO entityTO) {
69-
OIDCJWKS jwks = dao.get().orElse(null);
70-
if (jwks == null) {
71-
jwks = entityFactory.newEntity(OIDCJWKS.class);
72-
}
99+
OIDCJWKS jwks = oidcJWKSDAO.get().orElseGet(() -> entityFactory.newEntity(OIDCJWKS.class));
73100
jwks.setJson(entityTO.getJson());
74-
return binder.getOIDCJWKSTO(dao.save(jwks));
101+
return binder.getOIDCJWKSTO(oidcJWKSDAO.save(jwks));
75102
}
76103

77104
@PreAuthorize("hasRole('" + AMEntitlement.OIDC_JWKS_DELETE + "')")
78105
public void delete() {
79-
dao.delete();
106+
oidcJWKSDAO.delete();
80107
}
81108

82109
@Override
83110
protected OIDCJWKSTO resolveReference(final Method method, final Object... args)
84111
throws UnresolvedReferenceException {
85112

86-
OIDCJWKS jwks = dao.get().orElseThrow(UnresolvedReferenceException::new);
113+
OIDCJWKS jwks = oidcJWKSDAO.get().orElseThrow(UnresolvedReferenceException::new);
87114
return binder.getOIDCJWKSTO(jwks);
88115
}
89116
}

core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAOIDCJWKSDAO.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ public JPAOIDCJWKSDAO(final EntityManager entityManager) {
4343
@Override
4444
public Optional<OIDCJWKS> get() {
4545
try {
46-
TypedQuery<OIDCJWKS> query = entityManager.
47-
createQuery("SELECT e FROM " + JPAOIDCJWKS.class.getSimpleName() + " e", OIDCJWKS.class);
46+
TypedQuery<OIDCJWKS> query = entityManager.createQuery(
47+
"SELECT e FROM " + JPAOIDCJWKS.class.getSimpleName() + " e", OIDCJWKS.class);
4848
return Optional.ofNullable(query.getSingleResult());
4949
} catch (NoResultException e) {
5050
LOG.debug("No OIDC JWKS found", e);
@@ -59,8 +59,6 @@ public OIDCJWKS save(final OIDCJWKS jwks) {
5959

6060
@Override
6161
public void delete() {
62-
entityManager.
63-
createQuery("DELETE FROM " + JPAOIDCJWKS.class.getSimpleName()).
64-
executeUpdate();
62+
entityManager.createQuery("DELETE FROM " + JPAOIDCJWKS.class.getSimpleName()).executeUpdate();
6563
}
6664
}

0 commit comments

Comments
 (0)