From 8d16a4d3bcaeacab70f0a06ba97114306c39fba6 Mon Sep 17 00:00:00 2001 From: Samuel Garofalo <72073457+SamuelGaro@users.noreply.github.com> Date: Wed, 15 Nov 2023 15:25:04 +0100 Subject: [PATCH] [SYNCOPE-1788] Allow JWKS value for OIDC client applications (#546) --- .../ClientAppModalPanelBuilder.java | 51 +++++++++++++++ .../ClientAppDirectoryPanel.properties | 3 + .../ClientAppDirectoryPanel_fr_CA.properties | 3 + .../ClientAppDirectoryPanel_it.properties | 3 + .../ClientAppDirectoryPanel_ja.properties | 3 + .../ClientAppDirectoryPanel_pt_BR.properties | 3 + .../ClientAppDirectoryPanel_ru.properties | 3 + .../common/lib/to/OIDCRPClientAppTO.java | 63 +++++++++++++++---- .../types/OIDCClientAuthenticationMethod.java | 28 +++++++++ .../api/entity/am/OIDCRPClientApp.java | 14 +++++ .../jpa/entity/am/JPAOIDCRPClientApp.java | 40 ++++++++++++ .../java/data/ClientAppDataBinderImpl.java | 6 ++ .../mapping/OIDCRPClientAppTOMapper.java | 7 +++ 13 files changed, 215 insertions(+), 12 deletions(-) create mode 100644 common/am/lib/src/main/java/org/apache/syncope/common/lib/types/OIDCClientAuthenticationMethod.java diff --git a/client/am/console/src/main/java/org/apache/syncope/client/console/clientapps/ClientAppModalPanelBuilder.java b/client/am/console/src/main/java/org/apache/syncope/client/console/clientapps/ClientAppModalPanelBuilder.java index ef538d73ab..6d0d09bd35 100644 --- a/client/am/console/src/main/java/org/apache/syncope/client/console/clientapps/ClientAppModalPanelBuilder.java +++ b/client/am/console/src/main/java/org/apache/syncope/client/console/clientapps/ClientAppModalPanelBuilder.java @@ -18,7 +18,9 @@ */ package org.apache.syncope.client.console.clientapps; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Base64; import java.util.Comparator; import java.util.Iterator; import java.util.LinkedHashMap; @@ -26,6 +28,7 @@ import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.ws.rs.core.MediaType; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; import org.apache.syncope.client.console.SyncopeConsoleSession; @@ -37,6 +40,7 @@ import org.apache.syncope.client.console.rest.RealmRestClient; import org.apache.syncope.client.console.wicket.markup.html.bootstrap.dialog.BaseModal; import org.apache.syncope.client.console.wicket.markup.html.form.AjaxSearchFieldPanel; +import org.apache.syncope.client.console.wicket.markup.html.form.BinaryFieldPanel; import org.apache.syncope.client.console.wicket.markup.html.form.MultiFieldPanel; import org.apache.syncope.client.console.wicket.markup.html.form.PolicyRenderer; import org.apache.syncope.client.ui.commons.Constants; @@ -53,9 +57,11 @@ import org.apache.syncope.common.lib.OIDCScopeConstants; import org.apache.syncope.common.lib.policy.PolicyTO; import org.apache.syncope.common.lib.to.ClientAppTO; +import org.apache.syncope.common.lib.to.OIDCRPClientAppTO; import org.apache.syncope.common.lib.to.RealmTO; import org.apache.syncope.common.lib.types.ClientAppType; import org.apache.syncope.common.lib.types.LogoutType; +import org.apache.syncope.common.lib.types.OIDCClientAuthenticationMethod; import org.apache.syncope.common.lib.types.OIDCGrantType; import org.apache.syncope.common.lib.types.OIDCResponseType; import org.apache.syncope.common.lib.types.OIDCSubjectType; @@ -344,6 +350,51 @@ protected Iterator getChoices(final String input) { "field", "logoutUri", new PropertyModel<>(clientAppTO, "logoutUri"), false); logoutUri.addValidator(new UrlValidator()); fields.add(logoutUri); + + BinaryFieldPanel jwks = new BinaryFieldPanel( + "field", + "jwks", + new Model<>() { + + private static final long serialVersionUID = 7666049400663637482L; + + @Override + public String getObject() { + OIDCRPClientAppTO oidcRPCA = (OIDCRPClientAppTO) clientAppTO; + return StringUtils.isBlank(oidcRPCA.getJwks()) + ? null + : Base64.getEncoder().encodeToString( + oidcRPCA.getJwks().getBytes(StandardCharsets.UTF_8)); + } + + @Override + public void setObject(final String object) { + OIDCRPClientAppTO oidcRPCA = (OIDCRPClientAppTO) clientAppTO; + if (StringUtils.isBlank(object)) { + oidcRPCA.setJwks(null); + } else { + oidcRPCA.setJwks( + new String(Base64.getDecoder().decode(object), StandardCharsets.UTF_8)); + } + } + }, + MediaType.APPLICATION_JSON, + "client-jwks"); + fields.add(jwks); + + AjaxTextFieldPanel jwksUri = new AjaxTextFieldPanel( + "field", "jwksUri", new PropertyModel<>(clientAppTO, "jwksUri"), false); + jwksUri.addValidator(new UrlValidator()); + fields.add(jwksUri); + + AjaxDropDownChoicePanel tokenEndpointAuthenticationMethod = + new AjaxDropDownChoicePanel<>( + "field", + "tokenEndpointAuthenticationMethod", + new PropertyModel<>(clientAppTO, "tokenEndpointAuthenticationMethod"), + false); + tokenEndpointAuthenticationMethod.setChoices(List.of(OIDCClientAuthenticationMethod.values())); + fields.add(tokenEndpointAuthenticationMethod); break; case SAML2SP: diff --git a/client/am/console/src/main/resources/org/apache/syncope/client/console/clientapps/ClientAppDirectoryPanel.properties b/client/am/console/src/main/resources/org/apache/syncope/client/console/clientapps/ClientAppDirectoryPanel.properties index 8a90e272bd..49116e51d8 100644 --- a/client/am/console/src/main/resources/org/apache/syncope/client/console/clientapps/ClientAppDirectoryPanel.properties +++ b/client/am/console/src/main/resources/org/apache/syncope/client/console/clientapps/ClientAppDirectoryPanel.properties @@ -67,3 +67,6 @@ usernameAttributeProviderConf.title=Username Attribute Provider for ${name} ticketExpirationPolicy=Ticket Expiration Policy auditHistory.title=Configuration history logoutType=Logout Type +jwks=JWKS +jwksUri=JWKS URI +tokenEndpointAuthenticationMethod=Token Endpoint Authentication Method diff --git a/client/am/console/src/main/resources/org/apache/syncope/client/console/clientapps/ClientAppDirectoryPanel_fr_CA.properties b/client/am/console/src/main/resources/org/apache/syncope/client/console/clientapps/ClientAppDirectoryPanel_fr_CA.properties index afb22b5b51..13b5ea6270 100644 --- a/client/am/console/src/main/resources/org/apache/syncope/client/console/clientapps/ClientAppDirectoryPanel_fr_CA.properties +++ b/client/am/console/src/main/resources/org/apache/syncope/client/console/clientapps/ClientAppDirectoryPanel_fr_CA.properties @@ -67,3 +67,6 @@ usernameAttributeProviderConf.title=Username Attribute Provider for ${name} ticketExpirationPolicy=Ticket Expiration Policy auditHistory.title=Historique de configuration logoutType=Logout Type +jwks=JWKS +jwksUri=JWKS URI +tokenEndpointAuthenticationMethod=Token Endpoint Authentication Method diff --git a/client/am/console/src/main/resources/org/apache/syncope/client/console/clientapps/ClientAppDirectoryPanel_it.properties b/client/am/console/src/main/resources/org/apache/syncope/client/console/clientapps/ClientAppDirectoryPanel_it.properties index 0ca1bf1e08..8b3cebc799 100644 --- a/client/am/console/src/main/resources/org/apache/syncope/client/console/clientapps/ClientAppDirectoryPanel_it.properties +++ b/client/am/console/src/main/resources/org/apache/syncope/client/console/clientapps/ClientAppDirectoryPanel_it.properties @@ -67,3 +67,6 @@ usernameAttributeProviderConf.title=Username Attribute Provider per ${name} ticketExpirationPolicy=Politica Ticket Expiration auditHistory.title=Storico delle configurazioni logoutType=Tipo Logout +jwks=JWKS +jwksUri=JWKS URI +tokenEndpointAuthenticationMethod=Metodo di autenticazione dell'endpoint token diff --git a/client/am/console/src/main/resources/org/apache/syncope/client/console/clientapps/ClientAppDirectoryPanel_ja.properties b/client/am/console/src/main/resources/org/apache/syncope/client/console/clientapps/ClientAppDirectoryPanel_ja.properties index 824c6a4abe..aa63ddba37 100644 --- a/client/am/console/src/main/resources/org/apache/syncope/client/console/clientapps/ClientAppDirectoryPanel_ja.properties +++ b/client/am/console/src/main/resources/org/apache/syncope/client/console/clientapps/ClientAppDirectoryPanel_ja.properties @@ -67,3 +67,6 @@ usernameAttributeProviderConf.title=Username Attribute Provider for ${name} ticketExpirationPolicy=Ticket Expiration Policy auditHistory.title=\u8a2d\u5b9a\u5c65\u6b74 logoutType=Logout Type +jwks=JWKS +jwksUri=JWKS URI +tokenEndpointAuthenticationMethod=Token Endpoint Authentication Method diff --git a/client/am/console/src/main/resources/org/apache/syncope/client/console/clientapps/ClientAppDirectoryPanel_pt_BR.properties b/client/am/console/src/main/resources/org/apache/syncope/client/console/clientapps/ClientAppDirectoryPanel_pt_BR.properties index cf375f5886..aa4455ca36 100644 --- a/client/am/console/src/main/resources/org/apache/syncope/client/console/clientapps/ClientAppDirectoryPanel_pt_BR.properties +++ b/client/am/console/src/main/resources/org/apache/syncope/client/console/clientapps/ClientAppDirectoryPanel_pt_BR.properties @@ -67,3 +67,6 @@ usernameAttributeProviderConf.title=Username Attribute Provider for ${name} ticketExpirationPolicy=Ticket Expiration Policy auditHistory.title=Hist\u00f3rico de configura\u00e7\u00e3o logoutType=Logout Type +jwks=JWKS +jwksUri=JWKS URI +tokenEndpointAuthenticationMethod=Token Endpoint Authentication Method diff --git a/client/am/console/src/main/resources/org/apache/syncope/client/console/clientapps/ClientAppDirectoryPanel_ru.properties b/client/am/console/src/main/resources/org/apache/syncope/client/console/clientapps/ClientAppDirectoryPanel_ru.properties index ffed062b7e..5d93238893 100644 --- a/client/am/console/src/main/resources/org/apache/syncope/client/console/clientapps/ClientAppDirectoryPanel_ru.properties +++ b/client/am/console/src/main/resources/org/apache/syncope/client/console/clientapps/ClientAppDirectoryPanel_ru.properties @@ -68,3 +68,6 @@ usernameAttributeProviderConf.title=Username Attribute Provider for ${name} ticketExpirationPolicy=Ticket Expiration Policy auditHistory.title=\u0418\u0441\u0442\u043e\u0440\u0438\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 logoutType=Logout Type +jwks=JWKS +jwksUri=JWKS URI +tokenEndpointAuthenticationMethod=Token Endpoint Authentication Method diff --git a/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/OIDCRPClientAppTO.java b/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/OIDCRPClientAppTO.java index 97e68ae3f6..88ebf1e2d0 100644 --- a/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/OIDCRPClientAppTO.java +++ b/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/OIDCRPClientAppTO.java @@ -26,6 +26,7 @@ import java.util.List; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.syncope.common.lib.types.OIDCClientAuthenticationMethod; import org.apache.syncope.common.lib.types.OIDCGrantType; import org.apache.syncope.common.lib.types.OIDCResponseType; import org.apache.syncope.common.lib.types.OIDCSubjectType; @@ -53,10 +54,17 @@ public class OIDCRPClientAppTO extends ClientAppTO { private final List scopes = new ArrayList<>(); - private String logoutUri; - private boolean bypassApprovalPrompt = true; + private String jwks; + + private String jwksUri; + + private OIDCClientAuthenticationMethod tokenEndpointAuthenticationMethod = + OIDCClientAuthenticationMethod.client_secret_basic; + + private String logoutUri; + @JacksonXmlProperty(localName = "_class", isAttribute = true) @JsonProperty("_class") @Schema(name = "_class", requiredMode = Schema.RequiredMode.REQUIRED, @@ -116,14 +124,6 @@ public void setSubjectType(final OIDCSubjectType subjectType) { this.subjectType = subjectType; } - public String getLogoutUri() { - return logoutUri; - } - - public void setLogoutUri(final String logoutUri) { - this.logoutUri = logoutUri; - } - public boolean isJwtAccessToken() { return jwtAccessToken; } @@ -146,6 +146,39 @@ public void setBypassApprovalPrompt(final boolean bypassApprovalPrompt) { this.bypassApprovalPrompt = bypassApprovalPrompt; } + public String getJwks() { + return jwks; + } + + public void setJwks(final String jwks) { + this.jwks = jwks; + } + + public String getJwksUri() { + return jwksUri; + } + + public void setJwksUri(final String jwksUri) { + this.jwksUri = jwksUri; + } + + public OIDCClientAuthenticationMethod getTokenEndpointAuthenticationMethod() { + return tokenEndpointAuthenticationMethod; + } + + public void setTokenEndpointAuthenticationMethod( + final OIDCClientAuthenticationMethod tokenEndpointAuthenticationMethod) { + this.tokenEndpointAuthenticationMethod = tokenEndpointAuthenticationMethod; + } + + public String getLogoutUri() { + return logoutUri; + } + + public void setLogoutUri(final String logoutUri) { + this.logoutUri = logoutUri; + } + @Override public boolean equals(final Object obj) { if (obj == null) { @@ -167,10 +200,13 @@ public boolean equals(final Object obj) { .append(this.redirectUris, rhs.redirectUris) .append(this.supportedGrantTypes, rhs.supportedGrantTypes) .append(this.supportedResponseTypes, rhs.supportedResponseTypes) - .append(this.logoutUri, rhs.logoutUri) .append(this.jwtAccessToken, rhs.jwtAccessToken) .append(this.scopes, rhs.scopes) .append(this.bypassApprovalPrompt, rhs.bypassApprovalPrompt) + .append(this.jwks, rhs.jwks) + .append(this.jwksUri, rhs.jwksUri) + .append(this.tokenEndpointAuthenticationMethod, rhs.tokenEndpointAuthenticationMethod) + .append(this.logoutUri, rhs.logoutUri) .isEquals(); } @@ -185,10 +221,13 @@ public int hashCode() { .append(redirectUris) .append(supportedGrantTypes) .append(supportedResponseTypes) - .append(logoutUri) .append(jwtAccessToken) .append(scopes) .append(bypassApprovalPrompt) + .append(jwks) + .append(jwksUri) + .append(tokenEndpointAuthenticationMethod) + .append(logoutUri) .toHashCode(); } } diff --git a/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/OIDCClientAuthenticationMethod.java b/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/OIDCClientAuthenticationMethod.java new file mode 100644 index 0000000000..c7165200b9 --- /dev/null +++ b/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/OIDCClientAuthenticationMethod.java @@ -0,0 +1,28 @@ +/* + * 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.common.lib.types; + +public enum OIDCClientAuthenticationMethod { + client_secret_basic, + client_secret_post, + client_secret_jwt, + private_key_jwt, + tls_client_auth; + +} diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/am/OIDCRPClientApp.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/am/OIDCRPClientApp.java index 4ae40a24ba..504bfd6d34 100644 --- a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/am/OIDCRPClientApp.java +++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/am/OIDCRPClientApp.java @@ -19,6 +19,7 @@ package org.apache.syncope.core.persistence.api.entity.am; import java.util.Set; +import org.apache.syncope.common.lib.types.OIDCClientAuthenticationMethod; import org.apache.syncope.common.lib.types.OIDCGrantType; import org.apache.syncope.common.lib.types.OIDCResponseType; import org.apache.syncope.common.lib.types.OIDCSubjectType; @@ -57,7 +58,20 @@ public interface OIDCRPClientApp extends ClientApp { void setSubjectType(OIDCSubjectType subjectType); + String getJwks(); + + void setJwks(String jwks); + + String getJwksUri(); + + void setJwksUri(String jwksUri); + + OIDCClientAuthenticationMethod getTokenEndpointAuthenticationMethod(); + + void setTokenEndpointAuthenticationMethod(OIDCClientAuthenticationMethod tokenEndpointAuthenticationMethod); + String getLogoutUri(); void setLogoutUri(String logoutUri); + } diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPAOIDCRPClientApp.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPAOIDCRPClientApp.java index 7fca26c315..563061554d 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPAOIDCRPClientApp.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPAOIDCRPClientApp.java @@ -33,6 +33,7 @@ import javax.persistence.PreUpdate; import javax.persistence.Table; import javax.persistence.Transient; +import org.apache.syncope.common.lib.types.OIDCClientAuthenticationMethod; import org.apache.syncope.common.lib.types.OIDCGrantType; import org.apache.syncope.common.lib.types.OIDCResponseType; import org.apache.syncope.common.lib.types.OIDCSubjectType; @@ -100,6 +101,13 @@ public class JPAOIDCRPClientApp extends AbstractClientApp implements OIDCRPClien @Transient private Set scopesSet = new HashSet<>(); + @Lob + private String jwks; + + private String jwksUri; + + private OIDCClientAuthenticationMethod tokenEndpointAuthenticationMethod; + private String logoutUri; @Override @@ -182,6 +190,38 @@ public Set getScopes() { return scopesSet; } + @Override + public String getJwks() { + return jwks; + } + + @Override + public void setJwks(final String jwks) { + this.jwks = jwks; + } + + @Override + public String getJwksUri() { + return jwksUri; + } + + @Override + public void setJwksUri(final String jwksUri) { + this.jwksUri = jwksUri; + } + + @Override + public OIDCClientAuthenticationMethod getTokenEndpointAuthenticationMethod() { + return tokenEndpointAuthenticationMethod; + } + + @Override + public void setTokenEndpointAuthenticationMethod( + final OIDCClientAuthenticationMethod tokenEndpointAuthenticationMethod) { + + this.tokenEndpointAuthenticationMethod = tokenEndpointAuthenticationMethod; + } + @Override public String getLogoutUri() { return logoutUri; diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/ClientAppDataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/ClientAppDataBinderImpl.java index 390ab3b964..7bebba11b6 100644 --- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/ClientAppDataBinderImpl.java +++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/ClientAppDataBinderImpl.java @@ -237,6 +237,9 @@ protected void doUpdate(final OIDCRPClientApp clientApp, final OIDCRPClientAppTO clientApp.getScopes().clear(); clientApp.getScopes().addAll(clientAppTO.getScopes()); clientApp.setLogoutUri(clientAppTO.getLogoutUri()); + clientApp.setJwks(clientAppTO.getJwks()); + clientApp.setJwksUri(clientAppTO.getJwksUri()); + clientApp.setTokenEndpointAuthenticationMethod(clientAppTO.getTokenEndpointAuthenticationMethod()); } protected OIDCRPClientAppTO getOIDCClientAppTO(final OIDCRPClientApp clientApp) { @@ -254,6 +257,9 @@ protected OIDCRPClientAppTO getOIDCClientAppTO(final OIDCRPClientApp clientApp) clientAppTO.setLogoutUri(clientApp.getLogoutUri()); clientAppTO.setJwtAccessToken(clientApp.isJwtAccessToken()); clientAppTO.setBypassApprovalPrompt(clientApp.isBypassApprovalPrompt()); + clientAppTO.setJwks(clientApp.getJwks()); + clientAppTO.setJwksUri(clientApp.getJwksUri()); + clientAppTO.setTokenEndpointAuthenticationMethod(clientApp.getTokenEndpointAuthenticationMethod()); return clientAppTO; } diff --git a/wa/starter/src/main/java/org/apache/syncope/wa/starter/mapping/OIDCRPClientAppTOMapper.java b/wa/starter/src/main/java/org/apache/syncope/wa/starter/mapping/OIDCRPClientAppTOMapper.java index 7f923b4e6b..d9eb49fc7c 100644 --- a/wa/starter/src/main/java/org/apache/syncope/wa/starter/mapping/OIDCRPClientAppTOMapper.java +++ b/wa/starter/src/main/java/org/apache/syncope/wa/starter/mapping/OIDCRPClientAppTOMapper.java @@ -23,6 +23,7 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; import org.apache.syncope.common.lib.OIDCScopeConstants; import org.apache.syncope.common.lib.to.ClientAppTO; import org.apache.syncope.common.lib.to.OIDCRPClientAppTO; @@ -83,12 +84,18 @@ public RegisteredService map( } service.setJwtAccessToken(rp.isJwtAccessToken()); service.setBypassApprovalPrompt(rp.isBypassApprovalPrompt()); + if (StringUtils.isNotBlank(rp.getJwksUri())) { + service.setJwks(rp.getJwksUri()); + } else { + service.setJwks(rp.getJwks()); + } service.setSupportedGrantTypes(rp.getSupportedGrantTypes().stream(). map(OIDCGrantType::name).collect(Collectors.toSet())); service.setSupportedResponseTypes(rp.getSupportedResponseTypes().stream(). map(OIDCResponseType::getExternalForm).collect(Collectors.toSet())); Optional.ofNullable(rp.getSubjectType()).ifPresent(st -> service.setSubjectType(st.name())); service.setLogoutUrl(rp.getLogoutUri()); + service.setTokenEndpointAuthenticationMethod(rp.getTokenEndpointAuthenticationMethod().name()); service.setScopes(new HashSet<>(rp.getScopes()));