Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pr-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:

strategy:
matrix:
java-version: [ 8, 11, 17 ]
java-version: [ 11, 17 ]

steps:
- uses: actions/checkout@v2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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) {

Expand Down Expand Up @@ -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);
}
Expand All @@ -421,7 +429,7 @@ public String importIDP(InputStream fileInputStream, Attachment fileDetail) {
try {
String tenantDomain = ContextLoader.getTenantDomainFromContext();
identityProvider = identityProviderManager.addIdPWithResourceId(
Comment on lines 430 to 431

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

Suggested change
String tenantDomain = ContextLoader.getTenantDomainFromContext();
identityProvider = identityProviderManager.addIdPWithResourceId(
String tenantDomain = ContextLoader.getTenantDomainFromContext();
log.info("Importing IDP to tenant domain: " + tenantDomain);
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);
}
Expand All @@ -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);
Expand All @@ -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;
}
}

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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Potential NullPointerException when iterating over IDP properties.

identityProvider.getIdpProperties() may return null, which would cause an NPE in the for-each loop.

         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
In
components/org.wso2.carbon.identity.api.server.idp/org.wso2.carbon.identity.api.server.idp.v1/src/main/java/org/wso2/carbon/identity/api/server/idp/v1/core/ServerIdpManagementService.java
around lines 495 to 501, guard against a potential NullPointerException by
checking whether identityProvider.getIdpProperties() is null before iterating;
if null, skip the loop or treat as empty array/list, otherwise iterate normally
and set certificate when a property with name Constants.JWKS_URI is found;
ensure the null check is concise and maintain existing behavior (break after
setting certificate).

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;
}

private void processFederatedAuthenticatorsForResidentIDPUpdate(IdentityProvider newIdentityProvider) {

try {
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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 " +
Expand Down
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.
*/

Choose a reason for hiding this comment

The 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 Log log = LogFactory.getLog(CertificateUtil.class);

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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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:

  1. If only base64-encoded certificates are expected, remove the fallback and propagate the exception:
-            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);
+            }
  1. If both formats are valid, add explicit format detection and logging:
             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
In components/org.wso2.carbon.identity.api.server.idp/.../CertificateUtil.java
around lines 73-78, the code silently falls back to treating certValue as UTF-8
bytes when Base64 decoding throws, which can produce incorrect thumbprints or
mask bad input; either (A) if only Base64 is expected, remove the fallback and
rethrow or propagate the IllegalArgumentException so callers can handle/report
invalid input, or (B) if both Base64 and raw binary are supported, implement
explicit detection (e.g., try Base64 and if it fails log a warning with the
input identifier, validate the resulting bytes as a DER/PEM certificate before
using them, and continue only if validation passes), and add descriptive logging
for either branch so malformed data isn’t silently accepted.


String pemText = new String(outerDecoded, StandardCharsets.UTF_8);
byte[] derBytes = getDerBytesFromPem(pemText);
if (derBytes.length == 0) {
derBytes = outerDecoded;
}
Comment on lines +87 to +91
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate certificate format before computing thumbprints.

When PEM parsing fails (returns empty array), the code falls back to using outerDecoded directly for thumbprint computation. If outerDecoded is not valid DER-encoded certificate data (e.g., it's PEM text with malformed delimiters or other non-certificate data), the computed thumbprint will not be a valid certificate thumbprint. This could lead to incorrect certificate identification.

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.

Committable suggestion skipped: line range outside the PR's diff.

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();
}
}