-
Notifications
You must be signed in to change notification settings - Fork 168
Fix certificate field in IDP export-import with json #1042
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,7 +21,11 @@ | |
| import com.fasterxml.jackson.core.JsonFactory; | ||
| import com.fasterxml.jackson.core.JsonProcessingException; | ||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import com.google.gson.ExclusionStrategy; | ||
| import com.google.gson.FieldAttributes; | ||
| import com.google.gson.Gson; | ||
| import com.google.gson.GsonBuilder; | ||
| import com.google.gson.JsonObject; | ||
| import org.apache.commons.beanutils.BeanUtils; | ||
| import org.apache.commons.collections.CollectionUtils; | ||
| import org.apache.commons.io.IOUtils; | ||
|
|
@@ -55,6 +59,7 @@ | |
| import org.wso2.carbon.identity.api.server.idp.v1.model.FederatedAuthenticatorPUTRequest; | ||
| import org.wso2.carbon.identity.api.server.idp.v1.model.FederatedAuthenticatorRequest; | ||
| import org.wso2.carbon.identity.api.server.idp.v1.model.IdPGroup; | ||
| import org.wso2.carbon.identity.api.server.idp.v1.model.IdentityProviderExportResponse; | ||
| import org.wso2.carbon.identity.api.server.idp.v1.model.IdentityProviderListItem; | ||
| import org.wso2.carbon.identity.api.server.idp.v1.model.IdentityProviderListResponse; | ||
| import org.wso2.carbon.identity.api.server.idp.v1.model.IdentityProviderPOSTRequest; | ||
|
|
@@ -78,6 +83,7 @@ | |
| import org.wso2.carbon.identity.api.server.idp.v1.model.ProvisioningClaim; | ||
| import org.wso2.carbon.identity.api.server.idp.v1.model.ProvisioningResponse; | ||
| import org.wso2.carbon.identity.api.server.idp.v1.model.Roles; | ||
| import org.wso2.carbon.identity.api.server.idp.v1.util.CertificateUtil; | ||
| import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkConstants; | ||
| import org.wso2.carbon.identity.application.common.ApplicationAuthenticatorService; | ||
| import org.wso2.carbon.identity.application.common.model.AccountLookupAttributeMappingConfig; | ||
|
|
@@ -191,6 +197,8 @@ public class ServerIdpManagementService { | |
|
|
||
| private static final Log log = LogFactory.getLog(ServerIdpManagementService.class); | ||
|
|
||
| private static final String IDP_EXPORT_SUPPORT_MULTIPLE_CERT = "IdentityProviders.SupportMultipleCertificateExport"; | ||
|
|
||
| public ServerIdpManagementService(IdentityProviderManager identityProviderManager, TemplateManager templateManager, | ||
| ClaimMetadataManagementService claimMetadataManagementService) { | ||
|
|
||
|
|
@@ -396,7 +404,7 @@ public FileContent exportIDP(String idpId, boolean excludeSecrets, String fileTy | |
|
|
||
| FileContent fileContent; | ||
| try { | ||
| fileContent = generateFileFromModel(fileType, idpToExport); | ||
| fileContent = generateFileFromModel(fileType, createIDPExportResponse(idpToExport)); | ||
| } catch (IdentityProviderManagementException e) { | ||
| throw handleIdPException(e, Constants.ErrorMessage.ERROR_CODE_ERROR_EXPORTING_IDP, idpId); | ||
| } | ||
|
|
@@ -421,7 +429,7 @@ public String importIDP(InputStream fileInputStream, Attachment fileDetail) { | |
| try { | ||
| String tenantDomain = ContextLoader.getTenantDomainFromContext(); | ||
| identityProvider = identityProviderManager.addIdPWithResourceId( | ||
| getIDPFromFile(fileInputStream, fileDetail), tenantDomain); | ||
| createIDPImportRequest(getIDPFromFile(fileInputStream, fileDetail)), tenantDomain); | ||
| } catch (IdentityProviderManagementException e) { | ||
| throw handleIdPException(e, Constants.ErrorMessage.ERROR_CODE_ERROR_IMPORTING_IDP, null); | ||
| } | ||
|
|
@@ -439,7 +447,7 @@ public void updateIDPFromFile(String identityProviderId, InputStream fileInputSt | |
|
|
||
| IdentityProvider identityProvider; | ||
| try { | ||
| identityProvider = getIDPFromFile(fileInputStream, fileDetail); | ||
| identityProvider = createIDPImportRequest(getIDPFromFile(fileInputStream, fileDetail)); | ||
| String tenantDomain = ContextLoader.getTenantDomainFromContext(); | ||
| if (RESIDENT_IDP_RESERVED_NAME.equals(identityProviderId)) { | ||
| processFederatedAuthenticatorsForResidentIDPUpdate(identityProvider); | ||
|
|
@@ -452,6 +460,79 @@ public void updateIDPFromFile(String identityProviderId, InputStream fileInputSt | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * This exclusion strategy is to skip the 'certificate' field derived from the IdentityProvider class while | ||
| * keeping the new 'certificate' type declared in IdentityProviderExportResponse class. | ||
| */ | ||
| private static class CertificateSkippingExclusionStrategy implements ExclusionStrategy { | ||
|
|
||
| @Override | ||
| public boolean shouldSkipField(FieldAttributes f) { | ||
| return f.getName().equals("certificate") && | ||
| f.getDeclaringClass() == IdentityProviderExportResponse.class; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean shouldSkipClass(Class<?> clazz) { | ||
| return false; | ||
| } | ||
| } | ||
mpmadhavig marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| private IdentityProvider createIDPExportResponse(IdentityProvider identityProvider) { | ||
|
|
||
| if (!Boolean.parseBoolean(IdentityUtil.getProperty(IDP_EXPORT_SUPPORT_MULTIPLE_CERT))) { | ||
| log.debug("IDP export/import with multiple certificates is not enabled. Exporting the IDP as is."); | ||
| return identityProvider; | ||
| } | ||
|
|
||
| Gson gson = new GsonBuilder() | ||
| .setExclusionStrategies(new CertificateSkippingExclusionStrategy()) | ||
| .create(); | ||
| JsonObject json = gson.toJsonTree(identityProvider).getAsJsonObject(); | ||
| IdentityProviderExportResponse exportResponse = gson.fromJson(json, IdentityProviderExportResponse.class); | ||
|
|
||
| Certificate certificate = null; | ||
| IdentityProviderProperty[] idpProperties = identityProvider.getIdpProperties(); | ||
| for (IdentityProviderProperty property : idpProperties) { | ||
| if (Constants.JWKS_URI.equals(property.getName())) { | ||
| certificate = new Certificate().jwksUri(property.getValue()); | ||
| break; | ||
| } | ||
| } | ||
|
Comment on lines
+495
to
+501
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potential NullPointerException when iterating over IDP properties.
Certificate certificate = null;
IdentityProviderProperty[] idpProperties = identityProvider.getIdpProperties();
- for (IdentityProviderProperty property : idpProperties) {
- if (Constants.JWKS_URI.equals(property.getName())) {
- certificate = new Certificate().jwksUri(property.getValue());
- break;
+ if (idpProperties != null) {
+ for (IdentityProviderProperty property : idpProperties) {
+ if (Constants.JWKS_URI.equals(property.getName())) {
+ certificate = new Certificate().jwksUri(property.getValue());
+ break;
+ }
}
}🤖 Prompt for AI Agents |
||
| if (certificate == null && ArrayUtils.isNotEmpty(identityProvider.getCertificateInfoArray())) { | ||
| List<String> certificates = new ArrayList<>(); | ||
| for (CertificateInfo certInfo : identityProvider.getCertificateInfoArray()) { | ||
| certificates.add(certInfo.getCertValue()); | ||
| } | ||
| certificate = new Certificate().certificates(certificates); | ||
| } | ||
|
|
||
| exportResponse.setCertificate(certificate); | ||
|
|
||
| return exportResponse; | ||
| } | ||
|
|
||
| private IdentityProvider createIDPImportRequest(IdentityProvider identityProvider) | ||
| throws IdentityProviderManagementClientException { | ||
|
|
||
| if (!Boolean.parseBoolean(IdentityUtil.getProperty(IDP_EXPORT_SUPPORT_MULTIPLE_CERT))) { | ||
| log.debug("IDP export/import with multiple certificates is not enabled. Importing the IDP as is."); | ||
| return identityProvider; | ||
| } | ||
|
|
||
| Gson gson = new GsonBuilder() | ||
| .setExclusionStrategies(new CertificateSkippingExclusionStrategy()) | ||
| .create(); | ||
| JsonObject json = gson.toJsonTree(identityProvider).getAsJsonObject(); | ||
| IdentityProvider importRequest = gson.fromJson(json, IdentityProvider.class); | ||
|
|
||
| String certificates = CertificateUtil.convertCertificateJsonString((( | ||
| (IdentityProviderExportResponse) identityProvider).getCertificates())); | ||
| importRequest.setCertificate(certificates); | ||
|
|
||
| return importRequest; | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| private void processFederatedAuthenticatorsForResidentIDPUpdate(IdentityProvider newIdentityProvider) { | ||
|
|
||
| try { | ||
|
|
@@ -3606,6 +3687,10 @@ private FileContent generateFileFromModel(String fileType, IdentityProvider iden | |
| private FileContent parseIdpToXml(IdentityProvider identityProvider) | ||
| throws IdentityProviderManagementException { | ||
|
|
||
| /* | ||
| Todo: when supporting MultipleCertificateExport for XML, update the below instance type | ||
| with identityProvider.getClass(). | ||
| */ | ||
| StringBuilder fileNameSB = new StringBuilder(identityProvider.getIdentityProviderName()); | ||
| fileNameSB.append(XML_FILE_EXTENSION); | ||
|
|
||
|
|
@@ -3641,6 +3726,10 @@ private FileContent parseIdpToJson(IdentityProvider identityProvider) | |
| private FileContent parseIdpToYaml(IdentityProvider identityProvider) | ||
| throws IdentityProviderManagementException { | ||
|
|
||
| /* | ||
| Todo: when supporting MultipleCertificateExport for XML, update the below instance type | ||
| with identityProvider.getClass(). | ||
| */ | ||
| StringBuilder fileNameSB = new StringBuilder(identityProvider.getIdentityProviderName()); | ||
| fileNameSB.append(YAML_FILE_EXTENSION); | ||
|
|
||
|
|
@@ -3737,6 +3826,9 @@ private IdentityProvider parseIdpFromJson(FileContent fileContent) | |
| throws IdentityProviderManagementClientException { | ||
|
|
||
| try { | ||
| if (Boolean.parseBoolean(IdentityUtil.getProperty(IDP_EXPORT_SUPPORT_MULTIPLE_CERT))) { | ||
| return new ObjectMapper().readValue(fileContent.getContent(), IdentityProviderExportResponse.class); | ||
| } | ||
| return new ObjectMapper().readValue(fileContent.getContent(), IdentityProvider.class); | ||
| } catch (JsonProcessingException e) { | ||
| throw new IdentityProviderManagementClientException(String.format("Error in reading JSON " + | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| /* | ||
| * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). | ||
| * | ||
| * WSO2 LLC. 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.wso2.carbon.identity.api.server.idp.v1.model; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonProperty; | ||
| import org.apache.commons.logging.Log; | ||
| import org.apache.commons.logging.LogFactory; | ||
| import org.wso2.carbon.identity.application.common.model.IdentityProvider; | ||
|
|
||
| import java.util.Objects; | ||
|
|
||
| /** | ||
| * Identity Provider Export Response model. | ||
| */ | ||
| public class IdentityProviderExportResponse extends IdentityProvider { | ||
|
|
||
| private static final long serialVersionUID = 1L; | ||
| private static final Log log = LogFactory.getLog(IdentityProviderExportResponse.class); | ||
|
|
||
| private Certificate certificate; | ||
|
|
||
| @JsonProperty("certificate") | ||
| public Certificate getCertificates() { | ||
|
|
||
| if (log.isDebugEnabled()) { | ||
| log.debug("Retrieving certificate from IdentityProviderExportResponse"); | ||
| } | ||
| return certificate; | ||
| } | ||
|
|
||
| @JsonProperty("certificate") | ||
| public void setCertificate(Certificate certificate) { | ||
|
|
||
| if (log.isDebugEnabled()) { | ||
| log.debug("Setting certificate in IdentityProviderExportResponse"); | ||
| } | ||
| this.certificate = certificate; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean equals(Object o) { | ||
|
|
||
| if (o == null || getClass() != o.getClass()) { | ||
| return false; | ||
| } | ||
| if (!super.equals(o)) { | ||
| return false; | ||
| } | ||
| IdentityProviderExportResponse that = (IdentityProviderExportResponse) o; | ||
| return Objects.equals(certificate, that.certificate); | ||
| } | ||
|
|
||
| @Override | ||
| public int hashCode() { | ||
|
|
||
| return Objects.hash(super.hashCode(), certificate); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,131 @@ | ||||||||||
| /* | ||||||||||
| * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). | ||||||||||
| * | ||||||||||
| * WSO2 LLC. 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.wso2.carbon.identity.api.server.idp.v1.util; | ||||||||||
|
|
||||||||||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||||||||||
| import com.fasterxml.jackson.databind.node.ArrayNode; | ||||||||||
| import org.apache.commons.logging.Log; | ||||||||||
| import org.apache.commons.logging.LogFactory; | ||||||||||
| import org.wso2.carbon.identity.api.server.idp.v1.model.Certificate; | ||||||||||
| import org.wso2.carbon.idp.mgt.IdentityProviderManagementClientException; | ||||||||||
|
|
||||||||||
| import java.nio.charset.StandardCharsets; | ||||||||||
| import java.security.MessageDigest; | ||||||||||
| import java.security.NoSuchAlgorithmException; | ||||||||||
| import java.util.Base64; | ||||||||||
| import java.util.List; | ||||||||||
| import java.util.regex.Matcher; | ||||||||||
| import java.util.regex.Pattern; | ||||||||||
|
|
||||||||||
| /** | ||||||||||
| * Utility class for Certificate related operations. | ||||||||||
| */ | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Log Improvement Suggestion No: 5
Suggested change
|
||||||||||
| public final class CertificateUtil { | ||||||||||
|
|
||||||||||
| private static final ObjectMapper MAPPER = new ObjectMapper(); | ||||||||||
| private static final Pattern PEM_INNER_BASE64 = | ||||||||||
| Pattern.compile("-----BEGIN CERTIFICATE-----(.*?)-----END CERTIFICATE-----", Pattern.DOTALL); | ||||||||||
| private static final Log log = LogFactory.getLog(CertificateUtil.class); | ||||||||||
|
|
||||||||||
| /** | ||||||||||
| * Convert incoming `Certificate` model (containing certificates list) to a JSON array string | ||||||||||
| * of objects with certValue and thumbPrint (sha-256 hex). | ||||||||||
| * <p> | ||||||||||
| * Example return: | ||||||||||
| * [ {"certValue":"...","thumbPrint":"..."}, {"certValue":"...","thumbPrint":"..."} ] | ||||||||||
| * | ||||||||||
| * @param certificate incoming Certificate model object | ||||||||||
| * @return JSON array string | ||||||||||
| * @throws IdentityProviderManagementClientException on digest errors | ||||||||||
| */ | ||||||||||
| public static String convertCertificateJsonString(Certificate certificate) | ||||||||||
| throws IdentityProviderManagementClientException { | ||||||||||
|
|
||||||||||
| if (log.isDebugEnabled()) { | ||||||||||
| log.debug("Converting certificate to JSON string format"); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| if (certificate == null) { | ||||||||||
| return "[]"; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| List<String> certificates = certificate.getCertificates(); | ||||||||||
| if (certificates == null || certificates.isEmpty()) { | ||||||||||
| return "[]"; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| ArrayNode resultArray = MAPPER.createArrayNode(); | ||||||||||
|
|
||||||||||
| for (String certValue : certificates) { | ||||||||||
| if (certValue == null) { | ||||||||||
| continue; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| byte[] outerDecoded; | ||||||||||
| try { | ||||||||||
| outerDecoded = Base64.getDecoder().decode(certValue); | ||||||||||
| } catch (IllegalArgumentException iae) { | ||||||||||
| outerDecoded = certValue.getBytes(StandardCharsets.UTF_8); | ||||||||||
| } | ||||||||||
|
Comment on lines
+80
to
+85
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clarify the base64 decoding fallback behavior. The fallback to UTF-8 bytes when base64 decoding fails could lead to processing non-certificate data and computing incorrect thumbprints. If certificates can legitimately arrive in multiple formats (base64-encoded vs. raw), this should be explicitly documented and validated. Otherwise, the silent fallback may hide data quality issues. Consider one of the following approaches:
- byte[] outerDecoded;
- try {
- outerDecoded = Base64.getDecoder().decode(certValue);
- } catch (IllegalArgumentException iae) {
- outerDecoded = certValue.getBytes(StandardCharsets.UTF_8);
- }
+ byte[] outerDecoded;
+ try {
+ outerDecoded = Base64.getDecoder().decode(certValue);
+ } catch (IllegalArgumentException e) {
+ throw new IdentityProviderManagementClientException(
+ "Invalid base64-encoded certificate value", e);
+ }
byte[] outerDecoded;
try {
outerDecoded = Base64.getDecoder().decode(certValue);
} catch (IllegalArgumentException iae) {
+ // Log or document: treating as raw certificate data
outerDecoded = certValue.getBytes(StandardCharsets.UTF_8);
}🤖 Prompt for AI Agents |
||||||||||
|
|
||||||||||
| String pemText = new String(outerDecoded, StandardCharsets.UTF_8); | ||||||||||
| byte[] derBytes = getDerBytesFromPem(pemText); | ||||||||||
| if (derBytes.length == 0) { | ||||||||||
| derBytes = outerDecoded; | ||||||||||
| } | ||||||||||
|
Comment on lines
+87
to
+91
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate certificate format before computing thumbprints. When PEM parsing fails (returns empty array), the code falls back to using Consider adding explicit validation or error handling: String pemText = new String(outerDecoded, StandardCharsets.UTF_8);
byte[] derBytes = getDerBytesFromPem(pemText);
if (derBytes.length == 0) {
- derBytes = outerDecoded;
+ // Attempt to use outerDecoded as raw DER, but validate first
+ // or throw an exception if certificate format cannot be determined
+ if (!isValidDerCertificate(outerDecoded)) {
+ throw new IdentityProviderManagementClientException(
+ "Cannot extract DER bytes from certificate value. " +
+ "Expected PEM format or raw DER encoding.");
+ }
+ derBytes = outerDecoded;
}Alternatively, if the fallback is intentional to support raw DER-encoded certificates, document this behavior clearly in the method's Javadoc.
|
||||||||||
| String thumbPrint; | ||||||||||
| try { | ||||||||||
| thumbPrint = sha256Hex(derBytes); | ||||||||||
| } catch (NoSuchAlgorithmException e) { | ||||||||||
| throw new IdentityProviderManagementClientException("Error while generating certificate thumbprint.", | ||||||||||
| e); | ||||||||||
| } | ||||||||||
| resultArray.addObject() | ||||||||||
| .put("certValue", certValue) | ||||||||||
| .put("thumbPrint", thumbPrint); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| return resultArray.toString(); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| private static byte[] getDerBytesFromPem(String pemText) { | ||||||||||
|
|
||||||||||
| Matcher m = PEM_INNER_BASE64.matcher(pemText); | ||||||||||
| if (m.find()) { | ||||||||||
| String innerBase64 = m.group(1); | ||||||||||
| innerBase64 = innerBase64.replaceAll("\\s+", ""); | ||||||||||
| try { | ||||||||||
| return Base64.getDecoder().decode(innerBase64); | ||||||||||
| } catch (IllegalArgumentException ignored) { | ||||||||||
| } | ||||||||||
| } | ||||||||||
| return new byte[0]; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| private static String sha256Hex(byte[] data) throws NoSuchAlgorithmException { | ||||||||||
|
|
||||||||||
| MessageDigest md = MessageDigest.getInstance("SHA-256"); | ||||||||||
| byte[] digest = md.digest(data); | ||||||||||
| StringBuilder sb = new StringBuilder(digest.length * 2); | ||||||||||
| for (byte b : digest) { | ||||||||||
| sb.append(String.format("%02x", b & 0xff)); | ||||||||||
| } | ||||||||||
| return sb.toString(); | ||||||||||
| } | ||||||||||
| } | ||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Log Improvement Suggestion No: 2