Skip to content

Commit bd33956

Browse files
committed
[SYNCOPE-1936] Generate OIDC JWKS as CAS does (#1252)
1 parent db10b3d commit bd33956

File tree

15 files changed

+354
-114
lines changed

15 files changed

+354
-114
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
@@ -24,6 +24,7 @@
2424
import java.util.concurrent.atomic.AtomicReference;
2525
import org.apache.syncope.client.console.SyncopeConsoleSession;
2626
import org.apache.syncope.client.console.rest.OIDCJWKSRestClient;
27+
import org.apache.syncope.client.console.rest.WAConfigRestClient;
2728
import org.apache.syncope.client.console.wicket.markup.html.bootstrap.dialog.BaseModal;
2829
import org.apache.syncope.client.console.wicket.markup.html.form.JsonEditorPanel;
2930
import org.apache.syncope.client.ui.commons.Constants;
@@ -54,6 +55,11 @@ public class OIDC extends Panel {
5455
@SpringBean
5556
protected OIDCJWKSRestClient oidcJWKSRestClient;
5657

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

5965
private static final long serialVersionUID = 389935548143327858L;
@@ -75,15 +81,15 @@ public OIDC(final String id, final String waPrefix, final PageReference pageRef)
7581
super(id);
7682
setOutputMarkupId(true);
7783

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

8587
AtomicReference<OIDCJWKSTO> oidcjwksto = oidcJWKSRestClient.get();
8688

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

8995
private static final long serialVersionUID = 6250423506463465679L;
@@ -123,18 +129,10 @@ protected void onComponentTag(final ComponentTag tag) {
123129

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

140138
@Override
@@ -184,6 +182,17 @@ protected void onComponentTag(final ComponentTag tag) {
184182
container.add(delete.setOutputMarkupId(true));
185183
MetaDataRoleAuthorizationStrategy.authorize(delete, ENABLE, AMEntitlement.OIDC_JWKS_DELETE);
186184

185+
generateModal.addSubmitButton();
186+
add(generateModal);
187+
generateModal.setWindowClosedCallback(target -> {
188+
oidcjwksto.set(oidcJWKSRestClient.get().get());
189+
view.setEnabled(oidcjwksto.get() != null);
190+
delete.setEnabled(oidcjwksto.get() != null);
191+
192+
target.add(container);
193+
generateModal.show(false);
194+
});
195+
187196
String wellKnownURI = waPrefix + "/oidc/.well-known/openid-configuration";
188197
container.add(new ExternalLink("wellKnownURI", wellKnownURI, wellKnownURI));
189198
}
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().get(0));
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().get(0));
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().get(0)));
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
@@ -37,8 +37,8 @@ public AtomicReference<OIDCJWKSTO> get() {
3737
return result;
3838
}
3939

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

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: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,12 @@ public ClientAppLogic clientAppLogic(
114114
@ConditionalOnMissingBean
115115
@Bean
116116
public OIDCJWKSLogic oidcJWKSLogic(
117-
final OIDCJWKSDataBinder binder,
118-
final OIDCJWKSDAO dao) {
117+
final OIDCJWKSDataBinder oidcJWKSDataBinder,
118+
final OIDCJWKSDAO oidcJWKSDAO,
119+
final WAConfigDAO waConfigDAO,
120+
final EntityFactory entityFactory) {
119121

120-
return new OIDCJWKSLogic(binder, dao);
122+
return new OIDCJWKSLogic(oidcJWKSDataBinder, oidcJWKSDAO, waConfigDAO, entityFactory);
121123
}
122124

123125
@ConditionalOnMissingBean

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

Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,17 @@
2020

2121
import java.lang.reflect.Method;
2222
import java.util.Optional;
23+
import java.util.List;
2324
import org.apache.syncope.common.lib.to.OIDCJWKSTO;
2425
import org.apache.syncope.common.lib.types.AMEntitlement;
2526
import org.apache.syncope.common.lib.types.IdRepoEntitlement;
2627
import org.apache.syncope.core.persistence.api.dao.DuplicateException;
2728
import org.apache.syncope.core.persistence.api.dao.NotFoundException;
2829
import org.apache.syncope.core.persistence.api.dao.OIDCJWKSDAO;
30+
import org.apache.syncope.core.persistence.api.dao.WAConfigDAO;
31+
import org.apache.syncope.core.persistence.api.entity.EntityFactory;
2932
import org.apache.syncope.core.persistence.api.entity.am.OIDCJWKS;
33+
import org.apache.syncope.core.persistence.api.entity.am.WAConfigEntry;
3034
import org.apache.syncope.core.provisioning.api.data.OIDCJWKSDataBinder;
3135
import org.springframework.security.access.prepost.PreAuthorize;
3236
import org.springframework.transaction.annotation.Transactional;
@@ -35,52 +39,81 @@ public class OIDCJWKSLogic extends AbstractTransactionalLogic<OIDCJWKSTO> {
3539

3640
protected final OIDCJWKSDataBinder binder;
3741

38-
protected final OIDCJWKSDAO dao;
42+
protected final OIDCJWKSDAO oidcJWKSDAO;
43+
44+
protected final WAConfigDAO waConfigDAO;
45+
46+
protected final EntityFactory entityFactory;
47+
48+
public OIDCJWKSLogic(
49+
final OIDCJWKSDataBinder binder,
50+
final OIDCJWKSDAO oidcJWKSDAO,
51+
final WAConfigDAO waConfigDAO,
52+
final EntityFactory entityFactory) {
3953

40-
public OIDCJWKSLogic(final OIDCJWKSDataBinder binder, final OIDCJWKSDAO dao) {
4154
this.binder = binder;
42-
this.dao = dao;
55+
this.oidcJWKSDAO = oidcJWKSDAO;
56+
this.waConfigDAO = waConfigDAO;
57+
this.entityFactory = entityFactory;
4358
}
4459

4560
@PreAuthorize("hasRole('" + AMEntitlement.OIDC_JWKS_READ + "') "
4661
+ "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
4762
@Transactional(readOnly = true)
4863
public OIDCJWKSTO get() {
49-
return Optional.ofNullable(dao.get()).
64+
return Optional.ofNullable(oidcJWKSDAO.get()).
5065
map(binder::getOIDCJWKSTO).
5166
orElseThrow(() -> new NotFoundException("OIDC JWKS not found"));
5267
}
5368

69+
@PreAuthorize("hasRole('" + AMEntitlement.OIDC_JWKS_SET + "') "
70+
+ "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
71+
public OIDCJWKSTO set(final OIDCJWKSTO entityTO) {
72+
OIDCJWKS jwks = oidcJWKSDAO.get();
73+
jwks.setJson(entityTO.getJson());
74+
return binder.getOIDCJWKSTO(oidcJWKSDAO.save(jwks));
75+
}
76+
5477
@PreAuthorize("hasRole('" + AMEntitlement.OIDC_JWKS_GENERATE + "') "
5578
+ "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
5679
public OIDCJWKSTO generate(final String jwksKeyId, final String jwksType, final int jwksKeySize) {
57-
OIDCJWKS jwks = dao.get();
58-
if (jwks == null) {
59-
return binder.getOIDCJWKSTO(dao.save(binder.create(jwksKeyId, jwksType, jwksKeySize)));
80+
if (oidcJWKSDAO.get() == null) {
81+
OIDCJWKSTO oidcJWKSTO = binder.getOIDCJWKSTO(
82+
oidcJWKSDAO.save(binder.create(jwksKeyId, jwksType, jwksKeySize)));
83+
84+
WAConfigEntry jwksKeyIdConfig = entityFactory.newEntity(WAConfigEntry.class);
85+
jwksKeyIdConfig.setKey("cas.authn.oidc.jwks.core.jwks-key-id");
86+
jwksKeyIdConfig.setValues(List.of(jwksKeyId));
87+
waConfigDAO.save(jwksKeyIdConfig);
88+
89+
WAConfigEntry jwksTypeConfig = entityFactory.newEntity(WAConfigEntry.class);
90+
jwksTypeConfig.setKey("cas.authn.oidc.jwks.core.jwks-type");
91+
jwksTypeConfig.setValues(List.of(jwksType));
92+
waConfigDAO.save(jwksTypeConfig);
93+
94+
WAConfigEntry jwksKeySizeConfig = entityFactory.newEntity(WAConfigEntry.class);
95+
jwksKeySizeConfig.setKey("cas.authn.oidc.jwks.core.jwks-key-size");
96+
jwksKeySizeConfig.setValues(List.of(String.valueOf(jwksKeySize)));
97+
waConfigDAO.save(jwksKeySizeConfig);
98+
99+
return oidcJWKSTO;
60100
}
101+
61102
throw new DuplicateException("OIDC JWKS already set");
62103
}
63104

64105
@PreAuthorize("hasRole('" + AMEntitlement.OIDC_JWKS_DELETE + "')")
65106
public void delete() {
66-
dao.delete();
107+
oidcJWKSDAO.delete();
67108
}
68109

69110
@Override
70111
protected OIDCJWKSTO resolveReference(final Method method, final Object... args)
71112
throws UnresolvedReferenceException {
72-
OIDCJWKS jwks = dao.get();
113+
OIDCJWKS jwks = oidcJWKSDAO.get();
73114
if (jwks == null) {
74115
throw new UnresolvedReferenceException();
75116
}
76117
return binder.getOIDCJWKSTO(jwks);
77118
}
78-
79-
@PreAuthorize("hasRole('" + AMEntitlement.OIDC_JWKS_SET + "') "
80-
+ "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
81-
public OIDCJWKSTO set(final OIDCJWKSTO entityTO) {
82-
OIDCJWKS jwks = dao.get();
83-
jwks.setJson(entityTO.getJson());
84-
return binder.getOIDCJWKSTO(dao.save(jwks));
85-
}
86119
}

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
@@ -31,8 +31,8 @@ public class JPAOIDCJWKSDAO extends AbstractDAO<OIDCJWKS> implements OIDCJWKSDAO
3131
@Override
3232
public OIDCJWKS get() {
3333
try {
34-
TypedQuery<OIDCJWKS> query = entityManager().
35-
createQuery("SELECT e FROM " + JPAOIDCJWKS.class.getSimpleName() + " e", OIDCJWKS.class);
34+
TypedQuery<OIDCJWKS> query = entityManager().createQuery(
35+
"SELECT e FROM " + JPAOIDCJWKS.class.getSimpleName() + " e", OIDCJWKS.class);
3636
return query.getSingleResult();
3737
} catch (final NoResultException e) {
3838
LOG.debug(e.getMessage());
@@ -47,8 +47,6 @@ public OIDCJWKS save(final OIDCJWKS jwks) {
4747

4848
@Override
4949
public void delete() {
50-
entityManager().
51-
createQuery("DELETE FROM " + JPAOIDCJWKS.class.getSimpleName()).
52-
executeUpdate();
50+
entityManager().createQuery("DELETE FROM " + JPAOIDCJWKS.class.getSimpleName()).executeUpdate();
5351
}
5452
}

0 commit comments

Comments
 (0)