From 2c0735564717bb185327055581560fd4036cf13e Mon Sep 17 00:00:00 2001 From: Md-Humair-KK Date: Wed, 21 Jan 2026 18:12:54 +0530 Subject: [PATCH 01/14] Data model and table update for Client encryption keys Signed-off-by: Md-Humair-KK --- .../java/io/mosip/esignet/entity/ClientDetail.java | 6 ++++++ .../esignet/services/ClientManagementServiceImpl.java | 10 ++++++++++ db_scripts/mosip_esignet/ddl/esignet-client_detail.sql | 2 ++ .../mosip_esignet/sql/1.7.1_to_1.8.0_rollback.sql | 6 ++++++ .../mosip_esignet/sql/1.7.1_to_1.8.0_upgrade.sql | 7 +++++++ .../java/io/mosip/esignet/core/dto/ClientDetail.java | 2 ++ .../esignet/core/dto/ClientDetailCreateRequestV3.java | 5 ++++- .../esignet/core/dto/ClientDetailUpdateRequestV3.java | 5 ++++- 8 files changed, 41 insertions(+), 2 deletions(-) diff --git a/client-management-service-impl/src/main/java/io/mosip/esignet/entity/ClientDetail.java b/client-management-service-impl/src/main/java/io/mosip/esignet/entity/ClientDetail.java index c42e1cd49..325f9624d 100644 --- a/client-management-service-impl/src/main/java/io/mosip/esignet/entity/ClientDetail.java +++ b/client-management-service-impl/src/main/java/io/mosip/esignet/entity/ClientDetail.java @@ -55,6 +55,12 @@ public class ClientDetail { @Column(name = "public_key_hash") private String publicKeyHash; + @Column(name = "enc_public_key") + private String encPublicKey; + + @Column(name = "enc_public_key_hash") + private String encPublicKeyHash; + @NotBlank(message = INVALID_CLAIM) @Column(name = "claims") private String claims; diff --git a/client-management-service-impl/src/main/java/io/mosip/esignet/services/ClientManagementServiceImpl.java b/client-management-service-impl/src/main/java/io/mosip/esignet/services/ClientManagementServiceImpl.java index 1b94e7d8c..5c1ee4320 100644 --- a/client-management-service-impl/src/main/java/io/mosip/esignet/services/ClientManagementServiceImpl.java +++ b/client-management-service-impl/src/main/java/io/mosip/esignet/services/ClientManagementServiceImpl.java @@ -211,6 +211,8 @@ public io.mosip.esignet.core.dto.ClientDetail getClientDetails(String clientId) dto.setLogoUri(result.get().getLogoUri()); dto.setStatus(result.get().getStatus()); dto.setPublicKey(result.get().getPublicKey()); + dto.setEncPublicKey(result.get().getEncPublicKey()); + dto.setEncPublicKeyHash(result.get().getEncPublicKeyHash()); dto.setAdditionalConfig(result.get().getAdditionalConfig()); TypeReference> typeReference = new TypeReference>() {}; try { @@ -313,12 +315,20 @@ public ClientDetail buildOAuthClient(String clientId, ClientDetailUpdateRequestV public ClientDetail buildClient(ClientDetailCreateRequestV3 clientDetailCreateRequestV3) { ClientDetail clientDetail = buildOAuthClient(clientDetailCreateRequestV3); clientDetail.setAdditionalConfig(clientDetailCreateRequestV3.getAdditionalConfig()); + if (clientDetailCreateRequestV3.getEncPublicKey() != null && !clientDetailCreateRequestV3.getEncPublicKey().isEmpty()) { + clientDetail.setEncPublicKey(IdentityProviderUtil.getJWKString(clientDetailCreateRequestV3.getEncPublicKey())); + clientDetail.setEncPublicKeyHash(identityProviderUtil.computePublicKeyHash(clientDetailCreateRequestV3.getEncPublicKey())); + } return clientDetail; } public ClientDetail buildClient(String clientId, ClientDetailUpdateRequestV3 clientDetailUpdateRequestV3) { ClientDetail clientDetail = buildOAuthClient(clientId, clientDetailUpdateRequestV3); clientDetail.setAdditionalConfig(clientDetailUpdateRequestV3.getAdditionalConfig()); + if (clientDetailUpdateRequestV3.getEncPublicKey() != null && !clientDetailUpdateRequestV3.getEncPublicKey().isEmpty()) { + clientDetail.setEncPublicKey(IdentityProviderUtil.getJWKString(clientDetailUpdateRequestV3.getEncPublicKey())); + clientDetail.setEncPublicKeyHash(identityProviderUtil.computePublicKeyHash(clientDetailUpdateRequestV3.getEncPublicKey())); + } return clientDetail; } diff --git a/db_scripts/mosip_esignet/ddl/esignet-client_detail.sql b/db_scripts/mosip_esignet/ddl/esignet-client_detail.sql index 95a37066c..2e11d2665 100644 --- a/db_scripts/mosip_esignet/ddl/esignet-client_detail.sql +++ b/db_scripts/mosip_esignet/ddl/esignet-client_detail.sql @@ -25,6 +25,8 @@ CREATE TABLE client_detail( acr_values varchar(1024) NOT NULL, public_key varchar(1024) NOT NULL, public_key_hash varchar(128) NOT NULL, + enc_public_key varchar(1024), + enc_public_key_hash varchar(128), grant_types varchar(512) NOT NULL, auth_methods varchar(512) NOT NULL, status varchar(20) NOT NULL, diff --git a/db_upgrade_script/mosip_esignet/sql/1.7.1_to_1.8.0_rollback.sql b/db_upgrade_script/mosip_esignet/sql/1.7.1_to_1.8.0_rollback.sql index 9aff7c3bd..109563299 100644 --- a/db_upgrade_script/mosip_esignet/sql/1.7.1_to_1.8.0_rollback.sql +++ b/db_upgrade_script/mosip_esignet/sql/1.7.1_to_1.8.0_rollback.sql @@ -34,6 +34,12 @@ ALTER TABLE client_detail ALTER COLUMN additional_config TYPE jsonb USING additional_config::jsonb; +-- Drop enc_public_key_hash column +ALTER TABLE client_detail DROP COLUMN IF EXISTS enc_public_key_hash; + +-- Drop enc_public_key column +ALTER TABLE client_detail DROP COLUMN IF EXISTS enc_public_key; + -- 3. Revert consent_detail column type changes ALTER TABLE consent_detail ALTER COLUMN id TYPE uuid USING id::uuid; ALTER TABLE consent_detail ALTER COLUMN client_id TYPE varchar; diff --git a/db_upgrade_script/mosip_esignet/sql/1.7.1_to_1.8.0_upgrade.sql b/db_upgrade_script/mosip_esignet/sql/1.7.1_to_1.8.0_upgrade.sql index 84dc9ea67..e41ccc8cf 100644 --- a/db_upgrade_script/mosip_esignet/sql/1.7.1_to_1.8.0_upgrade.sql +++ b/db_upgrade_script/mosip_esignet/sql/1.7.1_to_1.8.0_upgrade.sql @@ -102,6 +102,10 @@ ALTER TABLE client_detail ALTER COLUMN additional_config TYPE varchar(2048) USING additional_config::text; +-- Add enc_public_key and enc_public_key_hash columns to store encryption public key and computed hash in JWK format +ALTER TABLE client_detail ADD COLUMN IF NOT EXISTS enc_public_key varchar(1024); +ALTER TABLE client_detail ADD COLUMN IF NOT EXISTS enc_public_key_hash varchar(128); + -- Drop helper function DROP FUNCTION IF EXISTS compute_public_key_hash(jsonb); @@ -139,5 +143,8 @@ ALTER TABLE public_key_registry ALTER COLUMN public_key TYPE varchar(2500); ALTER TABLE public_key_registry ALTER COLUMN certificate TYPE varchar(4000); ALTER TABLE public_key_registry ALTER COLUMN thumbprint TYPE varchar(128); + + + END; $$; diff --git a/esignet-core/src/main/java/io/mosip/esignet/core/dto/ClientDetail.java b/esignet-core/src/main/java/io/mosip/esignet/core/dto/ClientDetail.java index 5a9ea43c5..38689b758 100644 --- a/esignet-core/src/main/java/io/mosip/esignet/core/dto/ClientDetail.java +++ b/esignet-core/src/main/java/io/mosip/esignet/core/dto/ClientDetail.java @@ -25,6 +25,8 @@ public class ClientDetail implements Serializable { private String logoUri; private List redirectUris; private String publicKey; + private String encPublicKey; + private String encPublicKeyHash; private List claims; private List acrValues; private String status; diff --git a/esignet-core/src/main/java/io/mosip/esignet/core/dto/ClientDetailCreateRequestV3.java b/esignet-core/src/main/java/io/mosip/esignet/core/dto/ClientDetailCreateRequestV3.java index 14789cdf6..e5432a63e 100644 --- a/esignet-core/src/main/java/io/mosip/esignet/core/dto/ClientDetailCreateRequestV3.java +++ b/esignet-core/src/main/java/io/mosip/esignet/core/dto/ClientDetailCreateRequestV3.java @@ -15,12 +15,15 @@ public class ClientDetailCreateRequestV3 extends ClientDetailCreateRequestV2 { @ClientAdditionalConfig JsonNode additionalConfig; + private Map encPublicKey; + public ClientDetailCreateRequestV3(String clientId, String clientName, Map publicKey, String relyingPartyId, List userClaims, List authContextRefs, String logoUri, List redirectUris, List grantTypes, List clientAuthMethods, - Map clientNameLangMap, JsonNode additionalConfig) { + Map clientNameLangMap, JsonNode additionalConfig, Map encPublicKey) { super(clientId, clientName, publicKey, relyingPartyId, userClaims, authContextRefs, logoUri, redirectUris, grantTypes, clientAuthMethods, clientNameLangMap); this.additionalConfig = additionalConfig; + this.encPublicKey = encPublicKey; } } diff --git a/esignet-core/src/main/java/io/mosip/esignet/core/dto/ClientDetailUpdateRequestV3.java b/esignet-core/src/main/java/io/mosip/esignet/core/dto/ClientDetailUpdateRequestV3.java index a3fb7203a..03b3c2bec 100644 --- a/esignet-core/src/main/java/io/mosip/esignet/core/dto/ClientDetailUpdateRequestV3.java +++ b/esignet-core/src/main/java/io/mosip/esignet/core/dto/ClientDetailUpdateRequestV3.java @@ -15,8 +15,11 @@ public class ClientDetailUpdateRequestV3 extends ClientDetailUpdateRequestV2 { @ClientAdditionalConfig private JsonNode additionalConfig; - public ClientDetailUpdateRequestV3(String logUri, List redirectUris, List userClaims, List authContextRefs, String status, List grantTypes, String clientName, List clientAuthMethods, Map clientNameLangMap, JsonNode additionalConfig) { + private Map encPublicKey; + + public ClientDetailUpdateRequestV3(String logUri, List redirectUris, List userClaims, List authContextRefs, String status, List grantTypes, String clientName, List clientAuthMethods, Map clientNameLangMap, JsonNode additionalConfig, Map encPublicKey) { super(logUri, redirectUris, userClaims, authContextRefs, status, grantTypes, clientName, clientAuthMethods, clientNameLangMap); this.additionalConfig = additionalConfig; + this.encPublicKey = encPublicKey; } } From 91ce55597e50a2f6f7c070895b6d625dbadfb5d0 Mon Sep 17 00:00:00 2001 From: Md-Humair-KK Date: Wed, 21 Jan 2026 19:20:53 +0530 Subject: [PATCH 02/14] removed from update changes Signed-off-by: Md-Humair-KK --- .../mosip/esignet/services/ClientManagementServiceImpl.java | 4 ---- .../mosip/esignet/core/dto/ClientDetailUpdateRequestV3.java | 5 +---- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/client-management-service-impl/src/main/java/io/mosip/esignet/services/ClientManagementServiceImpl.java b/client-management-service-impl/src/main/java/io/mosip/esignet/services/ClientManagementServiceImpl.java index 5c1ee4320..d41eb3b41 100644 --- a/client-management-service-impl/src/main/java/io/mosip/esignet/services/ClientManagementServiceImpl.java +++ b/client-management-service-impl/src/main/java/io/mosip/esignet/services/ClientManagementServiceImpl.java @@ -325,10 +325,6 @@ public ClientDetail buildClient(ClientDetailCreateRequestV3 clientDetailCreateRe public ClientDetail buildClient(String clientId, ClientDetailUpdateRequestV3 clientDetailUpdateRequestV3) { ClientDetail clientDetail = buildOAuthClient(clientId, clientDetailUpdateRequestV3); clientDetail.setAdditionalConfig(clientDetailUpdateRequestV3.getAdditionalConfig()); - if (clientDetailUpdateRequestV3.getEncPublicKey() != null && !clientDetailUpdateRequestV3.getEncPublicKey().isEmpty()) { - clientDetail.setEncPublicKey(IdentityProviderUtil.getJWKString(clientDetailUpdateRequestV3.getEncPublicKey())); - clientDetail.setEncPublicKeyHash(identityProviderUtil.computePublicKeyHash(clientDetailUpdateRequestV3.getEncPublicKey())); - } return clientDetail; } diff --git a/esignet-core/src/main/java/io/mosip/esignet/core/dto/ClientDetailUpdateRequestV3.java b/esignet-core/src/main/java/io/mosip/esignet/core/dto/ClientDetailUpdateRequestV3.java index 03b3c2bec..a3fb7203a 100644 --- a/esignet-core/src/main/java/io/mosip/esignet/core/dto/ClientDetailUpdateRequestV3.java +++ b/esignet-core/src/main/java/io/mosip/esignet/core/dto/ClientDetailUpdateRequestV3.java @@ -15,11 +15,8 @@ public class ClientDetailUpdateRequestV3 extends ClientDetailUpdateRequestV2 { @ClientAdditionalConfig private JsonNode additionalConfig; - private Map encPublicKey; - - public ClientDetailUpdateRequestV3(String logUri, List redirectUris, List userClaims, List authContextRefs, String status, List grantTypes, String clientName, List clientAuthMethods, Map clientNameLangMap, JsonNode additionalConfig, Map encPublicKey) { + public ClientDetailUpdateRequestV3(String logUri, List redirectUris, List userClaims, List authContextRefs, String status, List grantTypes, String clientName, List clientAuthMethods, Map clientNameLangMap, JsonNode additionalConfig) { super(logUri, redirectUris, userClaims, authContextRefs, status, grantTypes, clientName, clientAuthMethods, clientNameLangMap); this.additionalConfig = additionalConfig; - this.encPublicKey = encPublicKey; } } From 4daa0cdea5c1f3feef967b1611761d97eb57b416 Mon Sep 17 00:00:00 2001 From: Md-Humair-KK Date: Tue, 27 Jan 2026 16:56:39 +0530 Subject: [PATCH 03/14] removed changes from create Signed-off-by: Md-Humair-KK --- .../esignet/services/ClientManagementServiceImpl.java | 9 +++------ .../esignet/core/dto/ClientDetailCreateRequestV3.java | 3 +-- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/client-management-service-impl/src/main/java/io/mosip/esignet/services/ClientManagementServiceImpl.java b/client-management-service-impl/src/main/java/io/mosip/esignet/services/ClientManagementServiceImpl.java index e29520d31..4a41c78b6 100644 --- a/client-management-service-impl/src/main/java/io/mosip/esignet/services/ClientManagementServiceImpl.java +++ b/client-management-service-impl/src/main/java/io/mosip/esignet/services/ClientManagementServiceImpl.java @@ -211,8 +211,9 @@ public io.mosip.esignet.core.dto.ClientDetail getClientDetails(String clientId) dto.setLogoUri(result.get().getLogoUri()); dto.setStatus(result.get().getStatus()); dto.setPublicKey(result.get().getPublicKey()); - dto.setEncPublicKey(result.get().getEncPublicKey()); - dto.setEncPublicKeyHash(result.get().getEncPublicKeyHash()); + if(result.get().getEncPublicKey()!=null){ + dto.setEncPublicKey(result.get().getEncPublicKey()); + } dto.setAdditionalConfig(result.get().getAdditionalConfig()); TypeReference> typeReference = new TypeReference>() {}; try { @@ -315,10 +316,6 @@ public ClientDetail buildOAuthClient(String clientId, ClientDetailUpdateRequestV public ClientDetail buildClient(ClientDetailCreateRequestV3 clientDetailCreateRequestV3) { ClientDetail clientDetail = buildOAuthClient(clientDetailCreateRequestV3); clientDetail.setAdditionalConfig(clientDetailCreateRequestV3.getAdditionalConfig()); - if (clientDetailCreateRequestV3.getEncPublicKey() != null && !clientDetailCreateRequestV3.getEncPublicKey().isEmpty()) { - clientDetail.setEncPublicKey(IdentityProviderUtil.getJWKString(clientDetailCreateRequestV3.getEncPublicKey())); - clientDetail.setEncPublicKeyHash(identityProviderUtil.computePublicKeyHash(clientDetailCreateRequestV3.getEncPublicKey())); - } return clientDetail; } diff --git a/esignet-core/src/main/java/io/mosip/esignet/core/dto/ClientDetailCreateRequestV3.java b/esignet-core/src/main/java/io/mosip/esignet/core/dto/ClientDetailCreateRequestV3.java index e5432a63e..f7264fe64 100644 --- a/esignet-core/src/main/java/io/mosip/esignet/core/dto/ClientDetailCreateRequestV3.java +++ b/esignet-core/src/main/java/io/mosip/esignet/core/dto/ClientDetailCreateRequestV3.java @@ -20,10 +20,9 @@ public class ClientDetailCreateRequestV3 extends ClientDetailCreateRequestV2 { public ClientDetailCreateRequestV3(String clientId, String clientName, Map publicKey, String relyingPartyId, List userClaims, List authContextRefs, String logoUri, List redirectUris, List grantTypes, List clientAuthMethods, - Map clientNameLangMap, JsonNode additionalConfig, Map encPublicKey) { + Map clientNameLangMap, JsonNode additionalConfig) { super(clientId, clientName, publicKey, relyingPartyId, userClaims, authContextRefs, logoUri, redirectUris, grantTypes, clientAuthMethods, clientNameLangMap); this.additionalConfig = additionalConfig; - this.encPublicKey = encPublicKey; } } From c96cd5f506bddab3f6857943c5dd9e0f475260fa Mon Sep 17 00:00:00 2001 From: Md-Humair-KK Date: Tue, 27 Jan 2026 16:59:17 +0530 Subject: [PATCH 04/14] resolved conflicts Signed-off-by: Md-Humair-KK --- .../io/mosip/esignet/core/dto/ClientDetailCreateRequestV3.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/esignet-core/src/main/java/io/mosip/esignet/core/dto/ClientDetailCreateRequestV3.java b/esignet-core/src/main/java/io/mosip/esignet/core/dto/ClientDetailCreateRequestV3.java index f7264fe64..14789cdf6 100644 --- a/esignet-core/src/main/java/io/mosip/esignet/core/dto/ClientDetailCreateRequestV3.java +++ b/esignet-core/src/main/java/io/mosip/esignet/core/dto/ClientDetailCreateRequestV3.java @@ -15,8 +15,6 @@ public class ClientDetailCreateRequestV3 extends ClientDetailCreateRequestV2 { @ClientAdditionalConfig JsonNode additionalConfig; - private Map encPublicKey; - public ClientDetailCreateRequestV3(String clientId, String clientName, Map publicKey, String relyingPartyId, List userClaims, List authContextRefs, String logoUri, List redirectUris, List grantTypes, List clientAuthMethods, From 8b2c927ce0338692b9f4972ba802212dee99319e Mon Sep 17 00:00:00 2001 From: Md-Humair-KK Date: Wed, 28 Jan 2026 18:33:27 +0530 Subject: [PATCH 05/14] PATCH implementation for enc public keys Signed-off-by: Md-Humair-KK --- .../services/ClientManagementServiceImpl.java | 103 ++++- .../esignet/ClientManagementServiceTest.java | 419 +++++++++++++++++- docs/esignet-openapi.yaml | 194 ++++++++ .../core/dto/ClientDetailPatchRequest.java | 63 +++ .../core/spi/ClientManagementService.java | 15 + .../core/IdentityProviderUtilTest.java | 15 + .../io/mosip/esignet/api/util/Action.java | 1 + .../ClientManagementController.java | 26 ++ ...eSignet-with-mock.postman_environment.json | 6 + .../eSignet.postman_collection.json | 71 +++ 10 files changed, 910 insertions(+), 3 deletions(-) create mode 100644 esignet-core/src/main/java/io/mosip/esignet/core/dto/ClientDetailPatchRequest.java diff --git a/client-management-service-impl/src/main/java/io/mosip/esignet/services/ClientManagementServiceImpl.java b/client-management-service-impl/src/main/java/io/mosip/esignet/services/ClientManagementServiceImpl.java index 4a41c78b6..f77f6787a 100644 --- a/client-management-service-impl/src/main/java/io/mosip/esignet/services/ClientManagementServiceImpl.java +++ b/client-management-service-impl/src/main/java/io/mosip/esignet/services/ClientManagementServiceImpl.java @@ -37,7 +37,7 @@ import java.time.ZoneId; import java.util.*; -import static io.mosip.esignet.core.constants.Constants.*; +import static io.mosip.esignet.core.constants.Constants.CLIENT_ACTIVE_STATUS; @Slf4j @Service @@ -324,4 +324,105 @@ public ClientDetail buildClient(String clientId, ClientDetailUpdateRequestV3 cli clientDetail.setAdditionalConfig(clientDetailUpdateRequestV3.getAdditionalConfig()); return clientDetail; } + + /** + * Build client detail entity for PATCH update operation. + * Only non-null fields from the patch request will be applied. + * + * @param clientId The client ID to update + * @param patchRequest The patch request containing fields to update + * @return Updated ClientDetail entity + */ + public ClientDetail buildClient(String clientId, ClientDetailPatchRequest patchRequest) { + Optional result = clientDetailRepository.findById(clientId); + if (result.isEmpty()) { + log.error("Invalid Client Id : {}", ErrorConstants.INVALID_CLIENT_ID); + throw new EsignetException(ErrorConstants.INVALID_CLIENT_ID); + } + + ClientDetail clientDetail = result.get(); + + // Apply partial updates - only non-null fields + if (patchRequest.getLogoUri() != null) { + clientDetail.setLogoUri(patchRequest.getLogoUri()); + } + + if (patchRequest.getRedirectUris() != null) { + patchRequest.getRedirectUris().removeAll(NULL); + clientDetail.setRedirectUris(JSONArray.toJSONString(patchRequest.getRedirectUris())); + } + + if (patchRequest.getUserClaims() != null) { + patchRequest.getUserClaims().removeAll(NULL); + clientDetail.setClaims(JSONArray.toJSONString(patchRequest.getUserClaims())); + } + + if (patchRequest.getAuthContextRefs() != null) { + patchRequest.getAuthContextRefs().removeAll(NULL); + clientDetail.setAcrValues(JSONArray.toJSONString(patchRequest.getAuthContextRefs())); + } + + if (patchRequest.getStatus() != null) { + clientDetail.setStatus(patchRequest.getStatus()); + } + + if (patchRequest.getGrantTypes() != null) { + patchRequest.getGrantTypes().removeAll(NULL); + clientDetail.setGrantTypes(JSONArray.toJSONString(patchRequest.getGrantTypes())); + } + + if (patchRequest.getClientAuthMethods() != null) { + patchRequest.getClientAuthMethods().removeAll(NULL); + clientDetail.setClientAuthMethods(JSONArray.toJSONString(patchRequest.getClientAuthMethods())); + } + + // Handle client name update + if (patchRequest.getClientName() != null || patchRequest.getClientNameLangMap() != null) { + String existingName = clientDetail.getName(); + Map existingNameMap = new HashMap<>(); + try { + if (existingName != null) { + existingNameMap = objectMapper.readValue(existingName, new TypeReference>() {}); + } + } catch (Exception e) { + log.warn("Failed to parse existing client name as JSON, using empty map"); + } + + String clientName = patchRequest.getClientName() != null ? + patchRequest.getClientName() : + existingNameMap.getOrDefault(Constants.NONE_LANG_KEY, ""); + + Map clientNameLangMap = patchRequest.getClientNameLangMap() != null ? + patchRequest.getClientNameLangMap() : + existingNameMap; + + clientDetail.setName(getClientNameLanguageMapAsJsonString(clientNameLangMap, clientName)); + } + + if (patchRequest.getAdditionalConfig() != null) { + clientDetail.setAdditionalConfig(patchRequest.getAdditionalConfig()); + } + + // Handle enc_public_key update - only if provided and not empty + if (patchRequest.getEncPublicKey() != null && !patchRequest.getEncPublicKey().isEmpty()) { + clientDetail.setEncPublicKey(IdentityProviderUtil.getJWKString(patchRequest.getEncPublicKey())); + clientDetail.setEncPublicKeyHash(identityProviderUtil.computePublicKeyHash(patchRequest.getEncPublicKey())); + } + + clientDetail.setUpdatedtimes(LocalDateTime.now(ZoneId.of("UTC"))); + return clientDetail; + } + + @CacheEvict(value = Constants.CLIENT_DETAIL_CACHE, key = "#clientId") + @Override + public ClientDetailResponse patchClient(String clientId, ClientDetailPatchRequest patchRequest) throws EsignetException { + ClientDetail clientDetail = buildClient(clientId, patchRequest); + clientDetail = clientDetailRepository.save(clientDetail); + + auditWrapper.logAudit(AuditHelper.getClaimValue(SecurityContextHolder.getContext(), claimName), + Action.OAUTH_CLIENT_PATCH, ActionStatus.SUCCESS, AuditHelper.buildAuditDto(clientId), null); + + return getClientDetailResponse(clientDetail); + } } + diff --git a/client-management-service-impl/src/test/java/io/mosip/esignet/ClientManagementServiceTest.java b/client-management-service-impl/src/test/java/io/mosip/esignet/ClientManagementServiceTest.java index da7a9a994..0e6f7404e 100644 --- a/client-management-service-impl/src/test/java/io/mosip/esignet/ClientManagementServiceTest.java +++ b/client-management-service-impl/src/test/java/io/mosip/esignet/ClientManagementServiceTest.java @@ -6,8 +6,8 @@ package io.mosip.esignet; -import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.nimbusds.jose.Algorithm; import com.nimbusds.jose.jwk.JWK; import com.nimbusds.jose.jwk.KeyUse; @@ -27,7 +27,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.*; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.test.util.ReflectionTestUtils; import java.security.KeyPair; import java.security.KeyPairGenerator; @@ -58,10 +57,12 @@ public class ClientManagementServiceTest { IdentityProviderUtil identityProviderUtil; Map PUBLIC_KEY; + Map ENC_PUBLIC_KEY; @BeforeEach public void Before() { PUBLIC_KEY = generateJWK_RSA().toJSONObject(); + ENC_PUBLIC_KEY = generateEncryptionJWK_RSA().toJSONObject(); } @Test @@ -339,6 +340,401 @@ public void getClient_withInvalidClientId_thenFail() throws EsignetException { } } + @Test + public void patchClient_withValidClientId_thenPass() throws EsignetException { + ClientDetail clientDetail = createMockClientDetail("client_id_v1"); + Mockito.when(clientDetailRepository.findById("client_id_v1")).thenReturn(Optional.of(clientDetail)); + + ClientDetailPatchRequest patchRequest = new ClientDetailPatchRequest(); + patchRequest.setClientName("updated_client_name"); + patchRequest.setLogoUri("http://service.com/new_logo.png"); + patchRequest.setRedirectUris(Arrays.asList("http://service.com/callback")); + patchRequest.setStatus("ACTIVE"); + + ClientDetail savedEntity = new ClientDetail(); + savedEntity.setId("client_id_v1"); + savedEntity.setStatus("ACTIVE"); + Mockito.when(clientDetailRepository.save(Mockito.any(ClientDetail.class))).thenReturn(savedEntity); + + ClientDetailResponse response = clientManagementService.patchClient("client_id_v1", patchRequest); + + Assertions.assertNotNull(response); + Assertions.assertEquals("client_id_v1", response.getClientId()); + Assertions.assertEquals("ACTIVE", response.getStatus()); + } + + @Test + public void patchClient_withNonExistingClientId_thenFail() { + Mockito.when(clientDetailRepository.findById("non_existing_client")).thenReturn(Optional.empty()); + + ClientDetailPatchRequest patchRequest = new ClientDetailPatchRequest(); + patchRequest.setClientName("updated_name"); + + try { + clientManagementService.patchClient("non_existing_client", patchRequest); + Assertions.fail("Should have thrown EsignetException"); + } catch (EsignetException ex) { + Assertions.assertEquals(ErrorConstants.INVALID_CLIENT_ID, ex.getErrorCode()); + } + } + + @Test + public void patchClient_withEncPublicKey_thenPass() throws EsignetException { + ClientDetail clientDetail = createMockClientDetail("client_id_v1"); + Mockito.when(clientDetailRepository.findById("client_id_v1")).thenReturn(Optional.of(clientDetail)); + Mockito.when(identityProviderUtil.computePublicKeyHash(Mockito.any())).thenReturn("mock_hash_value"); + + ClientDetailPatchRequest patchRequest = new ClientDetailPatchRequest(); + patchRequest.setEncPublicKey(ENC_PUBLIC_KEY); + + ClientDetail savedEntity = new ClientDetail(); + savedEntity.setId("client_id_v1"); + savedEntity.setStatus("ACTIVE"); + savedEntity.setEncPublicKey("{\"kty\":\"RSA\"}"); + savedEntity.setEncPublicKeyHash("mock_hash_value"); + Mockito.when(clientDetailRepository.save(Mockito.any(ClientDetail.class))).thenReturn(savedEntity); + + ClientDetailResponse response = clientManagementService.patchClient("client_id_v1", patchRequest); + + Assertions.assertNotNull(response); + Assertions.assertEquals("client_id_v1", response.getClientId()); + + // Verify that identityProviderUtil.computePublicKeyHash was called + Mockito.verify(identityProviderUtil, Mockito.times(1)).computePublicKeyHash(Mockito.any()); + } + + @Test + public void patchClient_withEmptyEncPublicKey_thenNoChange() throws EsignetException { + ClientDetail clientDetail = createMockClientDetail("client_id_v1"); + Mockito.when(clientDetailRepository.findById("client_id_v1")).thenReturn(Optional.of(clientDetail)); + + ClientDetailPatchRequest patchRequest = new ClientDetailPatchRequest(); + patchRequest.setEncPublicKey(new HashMap<>()); // Empty map + + ClientDetail savedEntity = new ClientDetail(); + savedEntity.setId("client_id_v1"); + savedEntity.setStatus("ACTIVE"); + Mockito.when(clientDetailRepository.save(Mockito.any(ClientDetail.class))).thenReturn(savedEntity); + + ClientDetailResponse response = clientManagementService.patchClient("client_id_v1", patchRequest); + + Assertions.assertNotNull(response); + // Verify that identityProviderUtil.computePublicKeyHash was NOT called (since encPublicKey was empty) + Mockito.verify(identityProviderUtil, Mockito.never()).computePublicKeyHash(Mockito.any()); + } + + @Test + public void patchClient_withNullEncPublicKey_thenNoChange() throws EsignetException { + ClientDetail clientDetail = createMockClientDetail("client_id_v1"); + Mockito.when(clientDetailRepository.findById("client_id_v1")).thenReturn(Optional.of(clientDetail)); + + ClientDetailPatchRequest patchRequest = new ClientDetailPatchRequest(); + + ClientDetail savedEntity = new ClientDetail(); + savedEntity.setId("client_id_v1"); + savedEntity.setStatus("ACTIVE"); + Mockito.when(clientDetailRepository.save(Mockito.any(ClientDetail.class))).thenReturn(savedEntity); + + ClientDetailResponse response = clientManagementService.patchClient("client_id_v1", patchRequest); + + Assertions.assertNotNull(response); + // Verify that identityProviderUtil.computePublicKeyHash was NOT called + Mockito.verify(identityProviderUtil, Mockito.never()).computePublicKeyHash(Mockito.any()); + } + + @Test + public void patchClient_withAdditionalConfig_thenPass() throws EsignetException { + ClientDetail clientDetail = createMockClientDetail("client_id_v1"); + Mockito.when(clientDetailRepository.findById("client_id_v1")).thenReturn(Optional.of(clientDetail)); + + ClientDetailPatchRequest patchRequest = new ClientDetailPatchRequest(); + ObjectNode additionalConfig = objectMapper.createObjectNode(); + additionalConfig.put("userinfo_response_type", "JWS"); + additionalConfig.put("signup_banner_required", true); + additionalConfig.put("forgot_pwd_link_required", true); + additionalConfig.put("consent_expire_in_mins", 20); + additionalConfig.put("require_pushed_authorization_requests", false); + additionalConfig.put("dpop_bound_access_tokens", false); + additionalConfig.put("require_pkce", true); + patchRequest.setAdditionalConfig(additionalConfig); + + ClientDetail savedEntity = new ClientDetail(); + savedEntity.setId("client_id_v1"); + savedEntity.setStatus("ACTIVE"); + Mockito.when(clientDetailRepository.save(Mockito.any(ClientDetail.class))).thenReturn(savedEntity); + + ClientDetailResponse response = clientManagementService.patchClient("client_id_v1", patchRequest); + + Assertions.assertNotNull(response); + Assertions.assertEquals("client_id_v1", response.getClientId()); + } + + @Test + public void patchClient_withPartialUpdate_onlyLogoUri_thenPass() throws EsignetException { + ClientDetail clientDetail = createMockClientDetail("client_id_v1"); + Mockito.when(clientDetailRepository.findById("client_id_v1")).thenReturn(Optional.of(clientDetail)); + + ClientDetailPatchRequest patchRequest = new ClientDetailPatchRequest(); + patchRequest.setLogoUri("http://service.com/updated_logo.png"); + // All other fields are null - should not be updated + + ClientDetail savedEntity = new ClientDetail(); + savedEntity.setId("client_id_v1"); + savedEntity.setStatus("ACTIVE"); + Mockito.when(clientDetailRepository.save(Mockito.any(ClientDetail.class))).thenReturn(savedEntity); + + ClientDetailResponse response = clientManagementService.patchClient("client_id_v1", patchRequest); + + Assertions.assertNotNull(response); + Assertions.assertEquals("client_id_v1", response.getClientId()); + } + + @Test + public void patchClient_withPartialUpdate_onlyStatus_thenPass() throws EsignetException { + ClientDetail clientDetail = createMockClientDetail("client_id_v1"); + Mockito.when(clientDetailRepository.findById("client_id_v1")).thenReturn(Optional.of(clientDetail)); + + ClientDetailPatchRequest patchRequest = new ClientDetailPatchRequest(); + patchRequest.setStatus("INACTIVE"); + + ClientDetail savedEntity = new ClientDetail(); + savedEntity.setId("client_id_v1"); + savedEntity.setStatus("INACTIVE"); + Mockito.when(clientDetailRepository.save(Mockito.any(ClientDetail.class))).thenReturn(savedEntity); + + ClientDetailResponse response = clientManagementService.patchClient("client_id_v1", patchRequest); + + Assertions.assertNotNull(response); + Assertions.assertEquals("client_id_v1", response.getClientId()); + Assertions.assertEquals("INACTIVE", response.getStatus()); + } + + @Test + public void patchClient_withPartialUpdate_onlyRedirectUris_thenPass() throws EsignetException { + ClientDetail clientDetail = createMockClientDetail("client_id_v1"); + Mockito.when(clientDetailRepository.findById("client_id_v1")).thenReturn(Optional.of(clientDetail)); + + ClientDetailPatchRequest patchRequest = new ClientDetailPatchRequest(); + patchRequest.setRedirectUris(Arrays.asList("http://new-service.com/callback", "http://new-service.com/home")); + + ClientDetail savedEntity = new ClientDetail(); + savedEntity.setId("client_id_v1"); + savedEntity.setStatus("ACTIVE"); + Mockito.when(clientDetailRepository.save(Mockito.any(ClientDetail.class))).thenReturn(savedEntity); + + ClientDetailResponse response = clientManagementService.patchClient("client_id_v1", patchRequest); + + Assertions.assertNotNull(response); + Assertions.assertEquals("client_id_v1", response.getClientId()); + } + + @Test + public void patchClient_withPartialUpdate_onlyUserClaims_thenPass() throws EsignetException { + ClientDetail clientDetail = createMockClientDetail("client_id_v1"); + Mockito.when(clientDetailRepository.findById("client_id_v1")).thenReturn(Optional.of(clientDetail)); + + ClientDetailPatchRequest patchRequest = new ClientDetailPatchRequest(); + patchRequest.setUserClaims(Arrays.asList("name", "email", "phone_number")); + + ClientDetail savedEntity = new ClientDetail(); + savedEntity.setId("client_id_v1"); + savedEntity.setStatus("ACTIVE"); + Mockito.when(clientDetailRepository.save(Mockito.any(ClientDetail.class))).thenReturn(savedEntity); + + ClientDetailResponse response = clientManagementService.patchClient("client_id_v1", patchRequest); + + Assertions.assertNotNull(response); + Assertions.assertEquals("client_id_v1", response.getClientId()); + } + + @Test + public void patchClient_withPartialUpdate_onlyAuthContextRefs_thenPass() throws EsignetException { + ClientDetail clientDetail = createMockClientDetail("client_id_v1"); + Mockito.when(clientDetailRepository.findById("client_id_v1")).thenReturn(Optional.of(clientDetail)); + + ClientDetailPatchRequest patchRequest = new ClientDetailPatchRequest(); + patchRequest.setAuthContextRefs(Arrays.asList("mosip:idp:acr:generated-code", "mosip:idp:acr:password")); + + ClientDetail savedEntity = new ClientDetail(); + savedEntity.setId("client_id_v1"); + savedEntity.setStatus("ACTIVE"); + Mockito.when(clientDetailRepository.save(Mockito.any(ClientDetail.class))).thenReturn(savedEntity); + + ClientDetailResponse response = clientManagementService.patchClient("client_id_v1", patchRequest); + + Assertions.assertNotNull(response); + Assertions.assertEquals("client_id_v1", response.getClientId()); + } + + @Test + public void patchClient_withPartialUpdate_onlyGrantTypes_thenPass() throws EsignetException { + ClientDetail clientDetail = createMockClientDetail("client_id_v1"); + Mockito.when(clientDetailRepository.findById("client_id_v1")).thenReturn(Optional.of(clientDetail)); + + ClientDetailPatchRequest patchRequest = new ClientDetailPatchRequest(); + patchRequest.setGrantTypes(Arrays.asList("authorization_code")); + + ClientDetail savedEntity = new ClientDetail(); + savedEntity.setId("client_id_v1"); + savedEntity.setStatus("ACTIVE"); + Mockito.when(clientDetailRepository.save(Mockito.any(ClientDetail.class))).thenReturn(savedEntity); + + ClientDetailResponse response = clientManagementService.patchClient("client_id_v1", patchRequest); + + Assertions.assertNotNull(response); + Assertions.assertEquals("client_id_v1", response.getClientId()); + } + + @Test + public void patchClient_withPartialUpdate_onlyClientAuthMethods_thenPass() throws EsignetException { + ClientDetail clientDetail = createMockClientDetail("client_id_v1"); + Mockito.when(clientDetailRepository.findById("client_id_v1")).thenReturn(Optional.of(clientDetail)); + + ClientDetailPatchRequest patchRequest = new ClientDetailPatchRequest(); + patchRequest.setClientAuthMethods(Arrays.asList("private_key_jwt")); + + ClientDetail savedEntity = new ClientDetail(); + savedEntity.setId("client_id_v1"); + savedEntity.setStatus("ACTIVE"); + Mockito.when(clientDetailRepository.save(Mockito.any(ClientDetail.class))).thenReturn(savedEntity); + + ClientDetailResponse response = clientManagementService.patchClient("client_id_v1", patchRequest); + + Assertions.assertNotNull(response); + Assertions.assertEquals("client_id_v1", response.getClientId()); + } + + @Test + public void patchClient_withClientNameLangMap_thenPass() throws EsignetException { + ClientDetail clientDetail = createMockClientDetail("client_id_v1"); + clientDetail.setName("{\"@none\":\"original_name\",\"eng\":\"Original Name\"}"); + Mockito.when(clientDetailRepository.findById("client_id_v1")).thenReturn(Optional.of(clientDetail)); + + ClientDetailPatchRequest patchRequest = new ClientDetailPatchRequest(); + patchRequest.setClientName("updated_client_name"); + Map langMap = new HashMap<>(); + langMap.put("eng", "Updated Client Name"); + langMap.put("fra", "Nom du Client Mis à Jour"); + patchRequest.setClientNameLangMap(langMap); + + ClientDetail savedEntity = new ClientDetail(); + savedEntity.setId("client_id_v1"); + savedEntity.setStatus("ACTIVE"); + Mockito.when(clientDetailRepository.save(Mockito.any(ClientDetail.class))).thenReturn(savedEntity); + + ClientDetailResponse response = clientManagementService.patchClient("client_id_v1", patchRequest); + + Assertions.assertNotNull(response); + Assertions.assertEquals("client_id_v1", response.getClientId()); + } + + @Test + public void patchClient_withOnlyClientName_thenPass() throws EsignetException { + ClientDetail clientDetail = createMockClientDetail("client_id_v1"); + clientDetail.setName("{\"@none\":\"original_name\"}"); + Mockito.when(clientDetailRepository.findById("client_id_v1")).thenReturn(Optional.of(clientDetail)); + + ClientDetailPatchRequest patchRequest = new ClientDetailPatchRequest(); + patchRequest.setClientName("new_client_name"); + + ClientDetail savedEntity = new ClientDetail(); + savedEntity.setId("client_id_v1"); + savedEntity.setStatus("ACTIVE"); + Mockito.when(clientDetailRepository.save(Mockito.any(ClientDetail.class))).thenReturn(savedEntity); + + ClientDetailResponse response = clientManagementService.patchClient("client_id_v1", patchRequest); + + Assertions.assertNotNull(response); + Assertions.assertEquals("client_id_v1", response.getClientId()); + } + + @Test + public void patchClient_withInvalidExistingClientName_thenPass() throws EsignetException { + ClientDetail clientDetail = createMockClientDetail("client_id_v1"); + clientDetail.setName("not_valid_json"); // Invalid JSON for name + Mockito.when(clientDetailRepository.findById("client_id_v1")).thenReturn(Optional.of(clientDetail)); + + ClientDetailPatchRequest patchRequest = new ClientDetailPatchRequest(); + patchRequest.setClientName("new_client_name"); + + ClientDetail savedEntity = new ClientDetail(); + savedEntity.setId("client_id_v1"); + savedEntity.setStatus("ACTIVE"); + Mockito.when(clientDetailRepository.save(Mockito.any(ClientDetail.class))).thenReturn(savedEntity); + + ClientDetailResponse response = clientManagementService.patchClient("client_id_v1", patchRequest); + + Assertions.assertNotNull(response); + Assertions.assertEquals("client_id_v1", response.getClientId()); + } + + @Test + public void patchClient_withAllFields_thenPass() throws EsignetException { + ClientDetail clientDetail = createMockClientDetail("client_id_v1"); + Mockito.when(clientDetailRepository.findById("client_id_v1")).thenReturn(Optional.of(clientDetail)); + Mockito.when(identityProviderUtil.computePublicKeyHash(Mockito.any())).thenReturn("mock_hash"); + + ClientDetailPatchRequest patchRequest = new ClientDetailPatchRequest(); + patchRequest.setLogoUri("http://service.com/new_logo.png"); + patchRequest.setRedirectUris(Arrays.asList("http://service.com/callback")); + patchRequest.setUserClaims(Arrays.asList("name", "email")); + patchRequest.setAuthContextRefs(Arrays.asList("mosip:idp:acr:generated-code")); + patchRequest.setStatus("ACTIVE"); + patchRequest.setGrantTypes(Arrays.asList("authorization_code")); + patchRequest.setClientName("full_update_name"); + Map langMap = new HashMap<>(); + langMap.put("eng", "Full Update Name"); + patchRequest.setClientNameLangMap(langMap); + patchRequest.setClientAuthMethods(Arrays.asList("private_key_jwt")); + patchRequest.setAdditionalConfig(objectMapper.createObjectNode().put("key", "value")); + patchRequest.setEncPublicKey(ENC_PUBLIC_KEY); + + ClientDetail savedEntity = new ClientDetail(); + savedEntity.setId("client_id_v1"); + savedEntity.setStatus("ACTIVE"); + Mockito.when(clientDetailRepository.save(Mockito.any(ClientDetail.class))).thenReturn(savedEntity); + + ClientDetailResponse response = clientManagementService.patchClient("client_id_v1", patchRequest); + + Assertions.assertNotNull(response); + Assertions.assertEquals("client_id_v1", response.getClientId()); + Assertions.assertEquals("ACTIVE", response.getStatus()); + Mockito.verify(identityProviderUtil, Mockito.times(1)).computePublicKeyHash(Mockito.any()); + } + + @Test + public void patchClient_withEmptyPatchRequest_thenOnlyUpdateTimestamp() throws EsignetException { + ClientDetail clientDetail = createMockClientDetail("client_id_v1"); + Mockito.when(clientDetailRepository.findById("client_id_v1")).thenReturn(Optional.of(clientDetail)); + + ClientDetailPatchRequest patchRequest = new ClientDetailPatchRequest(); + // All fields are null - only timestamp should be updated + + ClientDetail savedEntity = new ClientDetail(); + savedEntity.setId("client_id_v1"); + savedEntity.setStatus("ACTIVE"); + Mockito.when(clientDetailRepository.save(Mockito.any(ClientDetail.class))).thenReturn(savedEntity); + + ClientDetailResponse response = clientManagementService.patchClient("client_id_v1", patchRequest); + + Assertions.assertNotNull(response); + Assertions.assertEquals("client_id_v1", response.getClientId()); + } + + private ClientDetail createMockClientDetail(String clientId) { + ClientDetail clientDetail = new ClientDetail(); + clientDetail.setId(clientId); + clientDetail.setName("{\"@none\":\"test_client\"}"); + clientDetail.setLogoUri("http://service.com/logo.png"); + clientDetail.setClaims("[\"given_name\", \"birthdate\"]"); + clientDetail.setAcrValues("[\"mosip:idp:acr:static-code\"]"); + clientDetail.setClientAuthMethods("[\"private_key_jwt\"]"); + clientDetail.setGrantTypes("[\"authorization_code\"]"); + clientDetail.setRedirectUris("[\"https://service.com/home\",\"https://service.com/dashboard\"]"); + clientDetail.setStatus("ACTIVE"); + return clientDetail; + } + public static JWK generateJWK_RSA() { // Generate the RSA key pair try { @@ -357,4 +753,23 @@ public static JWK generateJWK_RSA() { } return null; } + + public static JWK generateEncryptionJWK_RSA() { + // Generate the RSA key pair for encryption + try { + KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA"); + gen.initialize(2048); + KeyPair keyPair = gen.generateKeyPair(); + // Convert public key to JWK format for encryption + return new RSAKey.Builder((RSAPublicKey)keyPair.getPublic()) + .privateKey((RSAPrivateKey)keyPair.getPrivate()) + .keyUse(KeyUse.ENCRYPTION) + .algorithm(new Algorithm("RSA-OAEP-256")) + .keyID(UUID.randomUUID().toString()) + .build(); + } catch (NoSuchAlgorithmException e) { + log.error("generateEncryptionJWK_RSA failed", e); + } + return null; + } } \ No newline at end of file diff --git a/docs/esignet-openapi.yaml b/docs/esignet-openapi.yaml index 9f396450a..5109e57f6 100644 --- a/docs/esignet-openapi.yaml +++ b/docs/esignet-openapi.yaml @@ -1417,6 +1417,200 @@ paths: - Authorization-update_oidc_client: [] servers: - url: 'https://esignet.collab.mosip.net/v1/esignet' + patch: + tags: + - Management + summary: Partially Update OAuth/OIDC Client Endpoint + description: |- + API to partially update existing OAuth/Open ID Connect (OIDC) client. Only provided fields will be updated. + + **Special handling for enc_public_key:** + - When set/updated: validates format and computes enc_public_key_hash + - When explicitly set to null: clears both enc_public_key and enc_public_key_hash + - When not present in request: leaves both fields unchanged + + **Authentication and authorization** is based on a valid JWT issued by a trusted IAM system including "**update_oidc_client**" scope. + operationId: patch-client-client_id + parameters: + - name: client_id + in: path + description: Client Identifier + required: true + schema: + type: string + examples: + - WMX5pO6dYdCFR3iaVWGclVPNxTNSADDv + requestBody: + description: Partial update request - only include fields to update + content: + application/json: + schema: + type: object + required: + - requestTime + - request + properties: + requestTime: + type: string + description: Current date and time when the request is sent + request: + type: object + description: All fields are optional. Only provided fields will be updated. + properties: + clientName: + type: string + description: Name of the OAuth/OIDC client. + minLength: 1 + maxLength: 256 + clientNameLangMap: + type: object + description: Client name in different languages. + status: + type: string + enum: + - ACTIVE + - INACTIVE + description: Status of the Client. + logoUri: + type: string + description: Relying party logo URI. + format: uri + redirectUris: + type: array + description: Valid list of callback URIs. + minItems: 1 + items: + type: string + format: uri + userClaims: + type: array + description: Allowed user info claims. + items: + type: string + authContextRefs: + type: array + description: Authentication Context Class Reference values. + items: + type: string + grantTypes: + type: array + description: Form of Authorization Grant. + items: + type: string + clientAuthMethods: + type: array + description: Auth method supported for token endpoint. + items: + type: string + additionalConfig: + type: object + description: Additional configuration for the client. + encPublicKey: + type: object + nullable: true + description: |- + Encryption public key in JWK format. + - Set to a valid JWK object to update + - Set to null to clear the encryption key + - Omit to leave unchanged + properties: + kty: + type: string + description: Key type (RSA or EC) + n: + type: string + description: RSA modulus (for RSA keys) + e: + type: string + description: RSA exponent (for RSA keys) + use: + type: string + description: Key use (enc for encryption) + alg: + type: string + description: Algorithm + kid: + type: string + description: Key ID + examples: + Update Status Only: + value: + requestTime: '2024-01-15T10:30:00.000Z' + request: + status: INACTIVE + Update Encryption Key: + value: + requestTime: '2024-01-15T10:30:00.000Z' + request: + encPublicKey: + kty: RSA + n: 0vx7agoebGcQSuuPiLJXZptN... + e: AQAB + use: enc + alg: RSA-OAEP-256 + kid: enc-key-1 + Clear Encryption Key: + value: + requestTime: '2024-01-15T10:30:00.000Z' + request: + encPublicKey: null + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + responseTime: + type: string + description: Date and time when the response is generated + response: + type: object + properties: + clientId: + type: string + description: Client identifier. + status: + type: string + enum: + - ACTIVE + - INACTIVE + required: + - clientId + errors: + type: array + description: List of Errors in case of request validation / processing failure. + items: + type: object + properties: + errorCode: + type: string + enum: + - invalid_client_id + - invalid_client_name + - invalid_claim + - invalid_acr + - invalid_uri + - invalid_redirect_uri + - invalid_grant_type + - invalid_client_auth + - invalid_public_key + - invalid_additional_config + errorMessage: + type: string + examples: + Success: + value: + responseTime: '2024-01-15T10:30:05.000Z' + response: + clientId: WMX5pO6dYdCFR3iaVWGclVPNxTNSADDv + status: ACTIVE + errors: [] + security: + - Authorization-update_oidc_client: [] + servers: + - url: 'https://esignet.collab.mosip.net/v1/esignet' /authorize: get: tags: diff --git a/esignet-core/src/main/java/io/mosip/esignet/core/dto/ClientDetailPatchRequest.java b/esignet-core/src/main/java/io/mosip/esignet/core/dto/ClientDetailPatchRequest.java new file mode 100644 index 000000000..0111e7c51 --- /dev/null +++ b/esignet-core/src/main/java/io/mosip/esignet/core/dto/ClientDetailPatchRequest.java @@ -0,0 +1,63 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package io.mosip.esignet.core.dto; + +import com.fasterxml.jackson.databind.JsonNode; +import io.mosip.esignet.core.constants.ErrorConstants; +import io.mosip.esignet.core.validator.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.URL; + +import jakarta.validation.constraints.*; +import java.util.List; +import java.util.Map; + +/** + * DTO for PATCH client request. Independent class with all updatable fields. + * All fields are optional - only provided fields will be updated. + * Note: Client ID is immutable and cannot be updated. + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ClientDetailPatchRequest { + + @URL(message = ErrorConstants.INVALID_URI) + private String logoUri; + + @Size(message = ErrorConstants.INVALID_REDIRECT_URI, min = 1, max = 5) + private List<@NotBlank(message = ErrorConstants.INVALID_REDIRECT_URI) + @RedirectURL String> redirectUris; + + @Size(message = ErrorConstants.INVALID_CLAIM, min = 1, max = 30) + private List<@OIDCClaim String> userClaims; + + @Size(message = ErrorConstants.INVALID_ACR, min = 1, max = 30) + private List<@AuthContextRef String> authContextRefs; + + @Pattern(regexp = "^(ACTIVE|INACTIVE)$", message = ErrorConstants.INVALID_STATUS) + private String status; + + @Size(message = ErrorConstants.UNSUPPORTED_GRANT_TYPE, min = 1, max = 3) + private List<@OIDCGrantType String> grantTypes; + + @Size(max = 256, message = ErrorConstants.INVALID_CLIENT_NAME) + private String clientName; + + @Size(message = ErrorConstants.INVALID_CLIENT_AUTH, min = 1, max = 3) + private List<@OIDCClientAuth String> clientAuthMethods; + + private Map<@ClientNameLang String, + @NotBlank(message = ErrorConstants.INVALID_CLIENT_NAME_MAP_VALUE) @Size(max = 50, + message = ErrorConstants.INVALID_CLIENT_NAME_LENGTH) String> clientNameLangMap; + + @ClientAdditionalConfig + private JsonNode additionalConfig; + + private Map encPublicKey; +} diff --git a/esignet-core/src/main/java/io/mosip/esignet/core/spi/ClientManagementService.java b/esignet-core/src/main/java/io/mosip/esignet/core/spi/ClientManagementService.java index 47f859d88..32780cfe2 100644 --- a/esignet-core/src/main/java/io/mosip/esignet/core/spi/ClientManagementService.java +++ b/esignet-core/src/main/java/io/mosip/esignet/core/spi/ClientManagementService.java @@ -84,4 +84,19 @@ public interface ClientManagementService { */ ClientDetailResponse updateOAuthClient(String clientId, ClientDetailUpdateRequestV2 clientDetailUpdateRequestV2) throws EsignetException; + /** + * API to partially update (PATCH) registered relying party client. + * + * Only provided fields will be updated. Supports special handling for enc_public_key: + * - When set/updated: validates format and computes enc_public_key_hash + * - When explicitly set to null: clears both enc_public_key and enc_public_key_hash + * - When not present in request: leaves both fields unchanged + * + * @param clientId The client ID to update (immutable) + * @param clientDetailPatchRequest The patch request containing fields to update + * @return ClientDetailResponse with clientId and status + * @throws EsignetException if client not found or validation fails + */ + ClientDetailResponse patchClient(String clientId, ClientDetailPatchRequest clientDetailPatchRequest) throws EsignetException; + } diff --git a/esignet-core/src/test/java/io/mosip/esignet/core/IdentityProviderUtilTest.java b/esignet-core/src/test/java/io/mosip/esignet/core/IdentityProviderUtilTest.java index 325cce131..e99d0eeb9 100644 --- a/esignet-core/src/test/java/io/mosip/esignet/core/IdentityProviderUtilTest.java +++ b/esignet-core/src/test/java/io/mosip/esignet/core/IdentityProviderUtilTest.java @@ -253,6 +253,21 @@ public void getJWKString_withInvalidUse_thenFail() { Assertions.assertEquals(ErrorConstants.INVALID_PUBLIC_KEY, ex.getMessage()); } + @Test + public void getJWKString_withValidEncryptionKey_thenPass() { + Map jwkMap = new HashMap<>(); + jwkMap.put("kty", "RSA"); + jwkMap.put("n", "oahUIzUup5kqncCkHk5Zb1pRrLx7e6YtM-9jX1f5e6mHnZFkC2LJUZ0sEh0n5Y5KnQfW9s7d7gK2b8P0EEl0h3ZyHkWzA3YbsgzB4pDxP4RxMZ1I8xD2z3UvfA1zjvKDHz6wEweq4hVJ8nS8GzZJ2E_vb3s"); + jwkMap.put("e", "AQAB"); + jwkMap.put("alg", "RSA-OAEP-256"); + jwkMap.put("use", "enc"); + + String jwkJson = IdentityProviderUtil.getJWKString(jwkMap); + Assertions.assertTrue(jwkJson.contains("\"kty\":\"RSA\"")); + Assertions.assertTrue(jwkJson.contains("\"use\":\"enc\"")); + Assertions.assertTrue(jwkJson.contains("\"alg\":\"RSA-OAEP-256\"")); + } + @Test public void getJWKString_withValidECKey_thenPass() { Map jwkMap = new HashMap<>(); diff --git a/esignet-integration-api/src/main/java/io/mosip/esignet/api/util/Action.java b/esignet-integration-api/src/main/java/io/mosip/esignet/api/util/Action.java index cd594b176..c001624df 100644 --- a/esignet-integration-api/src/main/java/io/mosip/esignet/api/util/Action.java +++ b/esignet-integration-api/src/main/java/io/mosip/esignet/api/util/Action.java @@ -5,6 +5,7 @@ public enum Action { OIDC_CLIENT_UPDATE("client-mgmt-service"), OAUTH_CLIENT_CREATE("client-mgmt-service"), OAUTH_CLIENT_UPDATE("client-mgmt-service"), + OAUTH_CLIENT_PATCH("client-mgmt-service"), GET_OAUTH_DETAILS("esignet-service"), GET_PAR_OAUTH_DETAILS("esignet-service"), TRANSACTION_STARTED("esignet-service"), diff --git a/esignet-service/src/main/java/io/mosip/esignet/controllers/ClientManagementController.java b/esignet-service/src/main/java/io/mosip/esignet/controllers/ClientManagementController.java index 5c821b8db..9b6989e3a 100644 --- a/esignet-service/src/main/java/io/mosip/esignet/controllers/ClientManagementController.java +++ b/esignet-service/src/main/java/io/mosip/esignet/controllers/ClientManagementController.java @@ -157,4 +157,30 @@ public ResponseWrapper updateClientV2(@Valid @PathVariab return response; } + /** + * PATCH endpoint to partially update client details. + * Only provided fields will be updated. Special handling for enc_public_key: + * - When set/updated: validates format and computes enc_public_key_hash + * - When explicitly set to null: clears both enc_public_key and enc_public_key_hash + * - When not present in request: leaves both fields unchanged + * + * @param clientId The client ID to update (immutable) + * @param requestWrapper The patch request containing fields to update + * @return Response with clientId and status + */ + @PatchMapping(value = "/client-mgmt/client/{client_id}", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseWrapper patchClient(@Valid @PathVariable("client_id") String clientId, + @Valid @RequestBody RequestWrapper requestWrapper) { + ResponseWrapper response = new ResponseWrapper<>(); + try { + response.setResponse(clientManagementService.patchClient(clientId, requestWrapper.getRequest())); + } catch (EsignetException ex) { + auditWrapper.logAudit(AuditHelper.getClaimValue(SecurityContextHolder.getContext(), claimName), + Action.OAUTH_CLIENT_PATCH, ActionStatus.ERROR, AuditHelper.buildAuditDto(clientId), ex); + throw ex; + } + response.setResponseTime(IdentityProviderUtil.getUTCDateTime()); + return response; + } + } diff --git a/postman-collection/eSignet-with-mock.postman_environment.json b/postman-collection/eSignet-with-mock.postman_environment.json index 05e1d2ef3..d56486d97 100644 --- a/postman-collection/eSignet-with-mock.postman_environment.json +++ b/postman-collection/eSignet-with-mock.postman_environment.json @@ -81,6 +81,12 @@ "type": "default", "enabled": true }, + { + "key": "encryption_public_key", + "value": "{\n \"kty\": \"RSA\",\n \"e\": \"AQAB\",\n \"use\": \"enc\",\n \"kid\": \"enc-key-1\",\n \"alg\": \"RSA-OAEP-256\",\n \"n\": \"pYVBMGj_RGmTR8m3BDc0P7Lmm73y7iX4AGv9bQP1DBL_f5R8LL6b8z1Q3N8xKjB7I_vIvH6xNGJPfZC0lIoHu3yvZ8_0IyBGQb4F-xNmI0sLo0SLVzKk0mXvwL1r5e2j7_WDJWMW_9S0e3bH4DXKW4iKcR0-6nhMt7j7L1YzL1SJHh8Fb3F7I3K0Y9E4zL2dXNrL2dI7bXPpL5vEQ7TfL4F8HvnPkI1qL5pJ7mI3F9z0b5X6vL8y0P2T7dN3L4dQ5M6P9R2K1xB7vN3L4dJ5H6M8P9Q2S3T5V7W8Y0Z1A2B4C6D8E0F2G4H6J8K0L2M4N6P8Q0R2S4T6U8V0W2X4Y6Z8\"\n}", + "type": "default", + "enabled": true + }, { "key": "claims_v3", "value": "{\n \"userinfo\": {\n \"name\": {\n \"essential\": false\n },\n \"phone_number\": {\n \"essential\": true\n },\n \"verified_claims\": [\n {\n \"verification\": {\n \"trust_framework\": null,\n \"time\": null\n },\n \"claims\": {\n \"name\": {\n \"essential\": true\n }\n }\n }\n ]\n },\n \"id_token\": {}\n}", diff --git a/postman-collection/eSignet.postman_collection.json b/postman-collection/eSignet.postman_collection.json index 1508ece77..cbb4acd5f 100644 --- a/postman-collection/eSignet.postman_collection.json +++ b/postman-collection/eSignet.postman_collection.json @@ -274,6 +274,10 @@ "publicKey_jwk.alg = \"RS256\"; // Or your preferred algorithm", "publicKey_jwk.use = \"sig\"; // Usually 'sig' for JWTs", "", + "// Add alg and use values to client public key", + "publicKey_jwk.alg = \"RS256\";", + "publicKey_jwk.use = \"sig\";", + "", "// Get the public key in PEM format", "const publicKeyPem = pmlib.rs.KEYUTIL.getPEM(kp.pubKeyObj, \"PKCS8PUB\");", "// Convert the PEM format to a simple Base64 string (removing PEM headers)", @@ -371,6 +375,73 @@ } }, "response": [] + }, + { + "name": "Patch OIDC client", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "eval(pm.environment.get('pmlib_code'))", + "", + "// Generate encryption key pair for encPublicKey only if not already available", + "const existingEncPublicKey = pm.environment.get(\"encryption_public_key\");", + "if (!existingEncPublicKey || existingEncPublicKey === '' || existingEncPublicKey === 'null' || existingEncPublicKey === '{}') {", + " enc_kp = pmlib.rs.KEYUTIL.generateKeypair(\"RSA\", 2048);", + " enc_privateKey_jwk = pmlib.rs.KEYUTIL.getJWK(enc_kp.prvKeyObj);", + " enc_publicKey_jwk = pmlib.rs.KEYUTIL.getJWK(enc_kp.pubKeyObj);", + "", + " // Add alg and use values to encryption public key", + " enc_publicKey_jwk.alg = \"RSA-OAEP-256\";", + " enc_publicKey_jwk.use = \"enc\";", + "", + " // Set the encryption keys in environment", + " pm.environment.set(\"encryption_private_key\", JSON.stringify(enc_privateKey_jwk));", + " pm.environment.set(\"encryption_public_key\", JSON.stringify(enc_publicKey_jwk));", + "}" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "PATCH", + "header": [ + { + "key": "X-XSRF-TOKEN", + "value": "{{csrf_token}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"requestTime\": \"{{$isoTimestamp}}\",\n \"request\": {\n \"clientName\": \"{{$randomCompanyName}}\",\n \"logoUri\": \"{{$randomImageUrl}}\",\n \"redirectUris\": [\n \"{{redirection_url}}\",\n \"io.mosip.residentapp://oauth\",\n \"http://localhost:5000/**\",\n \"http://localhost:3000/registration/*\"\n ],\n \"userClaims\": [\n \"name\",\n \"email\",\n \"gender\",\n \"phone_number\",\n \"birthdate\",\n \"picture\",\n \"address\"\n ],\n \"authContextRefs\": [\n \"mosip:idp:acr:generated-code\",\n \"mosip:idp:acr:password\",\n \"mosip:idp:acr:linked-wallet\",\n \"mosip:idp:acr:biometrics\",\n \"mosip:idp:acr:static-code\"\n ],\n \"status\": \"ACTIVE\",\n \"grantTypes\": [\n \"authorization_code\"\n ],\n \"clientAuthMethods\": [\n \"private_key_jwt\"\n ],\n \"additionalConfig\": {\n \"userinfo_response_type\": \"JWS\",\n \"purpose\": {\n \"type\": \"verify\",\n \"title\": {\n \"@none\": \"title\"\n },\n \"subTitle\": {\n \"@none\": \"subtitle\"\n }\n },\n \"signup_banner_required\": true,\n \"forgot_pwd_link_required\": true,\n \"consent_expire_in_mins\": 20,\n \"require_pushed_authorization_requests\": false,\n \"dpop_bound_access_tokens\": false,\n \"require_pkce\": true\n },\n \"encPublicKey\": {{encryption_public_key}}\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{url}}/v1/esignet/client-mgmt/client/{{client_id}}", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "esignet", + "client-mgmt", + "client", + "{{client_id}}" + ] + } + }, + "response": [] } ] } From 9547d611abb49e793dbbd6aaada169fc0d2dd16a Mon Sep 17 00:00:00 2001 From: Md-Humair-KK Date: Wed, 28 Jan 2026 19:02:34 +0530 Subject: [PATCH 06/14] fixed test case failures Signed-off-by: Md-Humair-KK --- .../services/ClientManagementServiceImpl.java | 2 - .../esignet/ClientDetailRepositoryTest.java | 87 +++++++++++++++++++ .../esignet/ClientManagementServiceTest.java | 8 +- .../src/test/resources/schema.sql | 2 + .../core/spi/ClientManagementService.java | 6 -- .../ClientManagementController.java | 5 -- esignet-service/src/test/resources/schema.sql | 2 + 7 files changed, 93 insertions(+), 19 deletions(-) diff --git a/client-management-service-impl/src/main/java/io/mosip/esignet/services/ClientManagementServiceImpl.java b/client-management-service-impl/src/main/java/io/mosip/esignet/services/ClientManagementServiceImpl.java index f77f6787a..66d30060c 100644 --- a/client-management-service-impl/src/main/java/io/mosip/esignet/services/ClientManagementServiceImpl.java +++ b/client-management-service-impl/src/main/java/io/mosip/esignet/services/ClientManagementServiceImpl.java @@ -327,8 +327,6 @@ public ClientDetail buildClient(String clientId, ClientDetailUpdateRequestV3 cli /** * Build client detail entity for PATCH update operation. - * Only non-null fields from the patch request will be applied. - * * @param clientId The client ID to update * @param patchRequest The patch request containing fields to update * @return Updated ClientDetail entity diff --git a/client-management-service-impl/src/test/java/io/mosip/esignet/ClientDetailRepositoryTest.java b/client-management-service-impl/src/test/java/io/mosip/esignet/ClientDetailRepositoryTest.java index 3f2c4dc1f..215198c93 100644 --- a/client-management-service-impl/src/test/java/io/mosip/esignet/ClientDetailRepositoryTest.java +++ b/client-management-service-impl/src/test/java/io/mosip/esignet/ClientDetailRepositoryTest.java @@ -289,4 +289,91 @@ public void createClientDetail_withInvalidStatus_thenFail() { } Assertions.fail(); } + + @Test + public void createClientDetail_withEncPublicKey_thenPass() { + ClientDetail clientDetail = new ClientDetail(); + clientDetail.setId("C02"); + clientDetail.setName("Client-02"); + clientDetail.setLogoUri("https://clienapp.com/logo.png"); + clientDetail.setStatus("ACTIVE"); + clientDetail.setRedirectUris("[\"https://clientapp.com/home\"]"); + clientDetail.setPublicKey("DUMMY PEM CERT"); + clientDetail.setPublicKeyHash(UUID.randomUUID().toString()); + clientDetail.setEncPublicKey("DUMMY ENC PUBLIC KEY"); + clientDetail.setEncPublicKeyHash(UUID.randomUUID().toString()); + clientDetail.setRpId("RP02"); + clientDetail.setClaims("[]"); + clientDetail.setAcrValues("[]"); + clientDetail.setGrantTypes("[\"authorization_code\"]"); + clientDetail.setClientAuthMethods("[\"private_key_jwt\"]"); + clientDetail.setCreatedtimes(LocalDateTime.now()); + clientDetail = clientDetailRepository.saveAndFlush(clientDetail); + Assertions.assertNotNull(clientDetail); + + Optional result = clientDetailRepository.findById("C02"); + Assertions.assertTrue(result.isPresent()); + Assertions.assertNotNull(result.get().getEncPublicKey()); + Assertions.assertNotNull(result.get().getEncPublicKeyHash()); + Assertions.assertEquals("DUMMY ENC PUBLIC KEY", result.get().getEncPublicKey()); + } + + @Test + public void createClientDetail_withNullEncPublicKey_thenPass() { + ClientDetail clientDetail = new ClientDetail(); + clientDetail.setId("C03"); + clientDetail.setName("Client-03"); + clientDetail.setLogoUri("https://clienapp.com/logo.png"); + clientDetail.setStatus("ACTIVE"); + clientDetail.setRedirectUris("[\"https://clientapp.com/home\"]"); + clientDetail.setPublicKey("DUMMY PEM CERT"); + clientDetail.setPublicKeyHash(UUID.randomUUID().toString()); + // encPublicKey and encPublicKeyHash are null (optional fields) + clientDetail.setRpId("RP03"); + clientDetail.setClaims("[]"); + clientDetail.setAcrValues("[]"); + clientDetail.setGrantTypes("[\"authorization_code\"]"); + clientDetail.setClientAuthMethods("[\"private_key_jwt\"]"); + clientDetail.setCreatedtimes(LocalDateTime.now()); + clientDetail = clientDetailRepository.saveAndFlush(clientDetail); + Assertions.assertNotNull(clientDetail); + + Optional result = clientDetailRepository.findById("C03"); + Assertions.assertTrue(result.isPresent()); + Assertions.assertNull(result.get().getEncPublicKey()); + Assertions.assertNull(result.get().getEncPublicKeyHash()); + } + + @Test + public void updateClientDetail_withEncPublicKey_thenPass() { + // First create a client without enc keys + ClientDetail clientDetail = new ClientDetail(); + clientDetail.setId("C04"); + clientDetail.setName("Client-04"); + clientDetail.setLogoUri("https://clienapp.com/logo.png"); + clientDetail.setStatus("ACTIVE"); + clientDetail.setRedirectUris("[\"https://clientapp.com/home\"]"); + clientDetail.setPublicKey("DUMMY PEM CERT"); + clientDetail.setPublicKeyHash(UUID.randomUUID().toString()); + clientDetail.setRpId("RP04"); + clientDetail.setClaims("[]"); + clientDetail.setAcrValues("[]"); + clientDetail.setGrantTypes("[\"authorization_code\"]"); + clientDetail.setClientAuthMethods("[\"private_key_jwt\"]"); + clientDetail.setCreatedtimes(LocalDateTime.now()); + clientDetail = clientDetailRepository.saveAndFlush(clientDetail); + Assertions.assertNotNull(clientDetail); + Assertions.assertNull(clientDetail.getEncPublicKey()); + + // Now update with enc keys + clientDetail.setEncPublicKey("UPDATED ENC PUBLIC KEY"); + clientDetail.setEncPublicKeyHash(UUID.randomUUID().toString()); + clientDetail.setUpdatedtimes(LocalDateTime.now()); + clientDetail = clientDetailRepository.saveAndFlush(clientDetail); + + Optional result = clientDetailRepository.findById("C04"); + Assertions.assertTrue(result.isPresent()); + Assertions.assertNotNull(result.get().getEncPublicKey()); + Assertions.assertEquals("UPDATED ENC PUBLIC KEY", result.get().getEncPublicKey()); + } } diff --git a/client-management-service-impl/src/test/java/io/mosip/esignet/ClientManagementServiceTest.java b/client-management-service-impl/src/test/java/io/mosip/esignet/ClientManagementServiceTest.java index 0e6f7404e..c9a73b740 100644 --- a/client-management-service-impl/src/test/java/io/mosip/esignet/ClientManagementServiceTest.java +++ b/client-management-service-impl/src/test/java/io/mosip/esignet/ClientManagementServiceTest.java @@ -372,7 +372,7 @@ public void patchClient_withNonExistingClientId_thenFail() { try { clientManagementService.patchClient("non_existing_client", patchRequest); - Assertions.fail("Should have thrown EsignetException"); + Assertions.fail(); } catch (EsignetException ex) { Assertions.assertEquals(ErrorConstants.INVALID_CLIENT_ID, ex.getErrorCode()); } @@ -399,7 +399,6 @@ public void patchClient_withEncPublicKey_thenPass() throws EsignetException { Assertions.assertNotNull(response); Assertions.assertEquals("client_id_v1", response.getClientId()); - // Verify that identityProviderUtil.computePublicKeyHash was called Mockito.verify(identityProviderUtil, Mockito.times(1)).computePublicKeyHash(Mockito.any()); } @@ -409,7 +408,7 @@ public void patchClient_withEmptyEncPublicKey_thenNoChange() throws EsignetExcep Mockito.when(clientDetailRepository.findById("client_id_v1")).thenReturn(Optional.of(clientDetail)); ClientDetailPatchRequest patchRequest = new ClientDetailPatchRequest(); - patchRequest.setEncPublicKey(new HashMap<>()); // Empty map + patchRequest.setEncPublicKey(new HashMap<>()); ClientDetail savedEntity = new ClientDetail(); savedEntity.setId("client_id_v1"); @@ -419,7 +418,6 @@ public void patchClient_withEmptyEncPublicKey_thenNoChange() throws EsignetExcep ClientDetailResponse response = clientManagementService.patchClient("client_id_v1", patchRequest); Assertions.assertNotNull(response); - // Verify that identityProviderUtil.computePublicKeyHash was NOT called (since encPublicKey was empty) Mockito.verify(identityProviderUtil, Mockito.never()).computePublicKeyHash(Mockito.any()); } @@ -438,7 +436,6 @@ public void patchClient_withNullEncPublicKey_thenNoChange() throws EsignetExcept ClientDetailResponse response = clientManagementService.patchClient("client_id_v1", patchRequest); Assertions.assertNotNull(response); - // Verify that identityProviderUtil.computePublicKeyHash was NOT called Mockito.verify(identityProviderUtil, Mockito.never()).computePublicKeyHash(Mockito.any()); } @@ -476,7 +473,6 @@ public void patchClient_withPartialUpdate_onlyLogoUri_thenPass() throws EsignetE ClientDetailPatchRequest patchRequest = new ClientDetailPatchRequest(); patchRequest.setLogoUri("http://service.com/updated_logo.png"); - // All other fields are null - should not be updated ClientDetail savedEntity = new ClientDetail(); savedEntity.setId("client_id_v1"); diff --git a/client-management-service-impl/src/test/resources/schema.sql b/client-management-service-impl/src/test/resources/schema.sql index 4e452fc52..28783e56e 100644 --- a/client-management-service-impl/src/test/resources/schema.sql +++ b/client-management-service-impl/src/test/resources/schema.sql @@ -8,6 +8,8 @@ CREATE TABLE IF NOT EXISTS client_detail( acr_values varchar(1024) NOT NULL, public_key varchar(1024) NOT NULL, public_key_hash varchar(128) NOT NULL, + enc_public_key varchar(2048), + enc_public_key_hash varchar(128), grant_types varchar(512) NOT NULL, auth_methods varchar(512) NOT NULL, status varchar(20) NOT NULL, diff --git a/esignet-core/src/main/java/io/mosip/esignet/core/spi/ClientManagementService.java b/esignet-core/src/main/java/io/mosip/esignet/core/spi/ClientManagementService.java index 32780cfe2..5c83bfcbd 100644 --- a/esignet-core/src/main/java/io/mosip/esignet/core/spi/ClientManagementService.java +++ b/esignet-core/src/main/java/io/mosip/esignet/core/spi/ClientManagementService.java @@ -86,12 +86,6 @@ public interface ClientManagementService { /** * API to partially update (PATCH) registered relying party client. - * - * Only provided fields will be updated. Supports special handling for enc_public_key: - * - When set/updated: validates format and computes enc_public_key_hash - * - When explicitly set to null: clears both enc_public_key and enc_public_key_hash - * - When not present in request: leaves both fields unchanged - * * @param clientId The client ID to update (immutable) * @param clientDetailPatchRequest The patch request containing fields to update * @return ClientDetailResponse with clientId and status diff --git a/esignet-service/src/main/java/io/mosip/esignet/controllers/ClientManagementController.java b/esignet-service/src/main/java/io/mosip/esignet/controllers/ClientManagementController.java index 9b6989e3a..593cdfff8 100644 --- a/esignet-service/src/main/java/io/mosip/esignet/controllers/ClientManagementController.java +++ b/esignet-service/src/main/java/io/mosip/esignet/controllers/ClientManagementController.java @@ -159,11 +159,6 @@ public ResponseWrapper updateClientV2(@Valid @PathVariab /** * PATCH endpoint to partially update client details. - * Only provided fields will be updated. Special handling for enc_public_key: - * - When set/updated: validates format and computes enc_public_key_hash - * - When explicitly set to null: clears both enc_public_key and enc_public_key_hash - * - When not present in request: leaves both fields unchanged - * * @param clientId The client ID to update (immutable) * @param requestWrapper The patch request containing fields to update * @return Response with clientId and status diff --git a/esignet-service/src/test/resources/schema.sql b/esignet-service/src/test/resources/schema.sql index 776bbaa5e..9eb9f8734 100644 --- a/esignet-service/src/test/resources/schema.sql +++ b/esignet-service/src/test/resources/schema.sql @@ -8,6 +8,8 @@ CREATE TABLE IF NOT EXISTS client_detail( acr_values varchar(1024) NOT NULL, public_key varchar(1024) NOT NULL, public_key_hash varchar(128) NOT NULL, + enc_public_key varchar(2048), + enc_public_key_hash varchar(128), grant_types varchar(512) NOT NULL, auth_methods varchar(512) NOT NULL, status varchar(20) NOT NULL, From 7924f0d415439ab349e730c48986dade9813e8d8 Mon Sep 17 00:00:00 2001 From: Md-Humair-KK Date: Thu, 29 Jan 2026 16:21:18 +0530 Subject: [PATCH 07/14] updated key size limit Signed-off-by: Md-Humair-KK --- client-management-service-impl/src/test/resources/schema.sql | 2 +- esignet-service/src/test/resources/schema.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client-management-service-impl/src/test/resources/schema.sql b/client-management-service-impl/src/test/resources/schema.sql index 28783e56e..f1d10c8af 100644 --- a/client-management-service-impl/src/test/resources/schema.sql +++ b/client-management-service-impl/src/test/resources/schema.sql @@ -8,7 +8,7 @@ CREATE TABLE IF NOT EXISTS client_detail( acr_values varchar(1024) NOT NULL, public_key varchar(1024) NOT NULL, public_key_hash varchar(128) NOT NULL, - enc_public_key varchar(2048), + enc_public_key varchar(1024), enc_public_key_hash varchar(128), grant_types varchar(512) NOT NULL, auth_methods varchar(512) NOT NULL, diff --git a/esignet-service/src/test/resources/schema.sql b/esignet-service/src/test/resources/schema.sql index 9eb9f8734..dccb07f87 100644 --- a/esignet-service/src/test/resources/schema.sql +++ b/esignet-service/src/test/resources/schema.sql @@ -8,7 +8,7 @@ CREATE TABLE IF NOT EXISTS client_detail( acr_values varchar(1024) NOT NULL, public_key varchar(1024) NOT NULL, public_key_hash varchar(128) NOT NULL, - enc_public_key varchar(2048), + enc_public_key varchar(1024), enc_public_key_hash varchar(128), grant_types varchar(512) NOT NULL, auth_methods varchar(512) NOT NULL, From 653732f39019681dd8d6431ab5d59c256302d190 Mon Sep 17 00:00:00 2001 From: Md-Humair-KK Date: Mon, 2 Feb 2026 14:00:55 +0530 Subject: [PATCH 08/14] updated PATCH details Signed-off-by: Md-Humair-KK --- docs/esignet-openapi.yaml | 64 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 5 deletions(-) diff --git a/docs/esignet-openapi.yaml b/docs/esignet-openapi.yaml index 5109e57f6..4d8d7fa7a 100644 --- a/docs/esignet-openapi.yaml +++ b/docs/esignet-openapi.yaml @@ -1465,6 +1465,10 @@ paths: clientNameLangMap: type: object description: Client name in different languages. + additionalProperties: + type: string + minLength: 1 + maxLength: 50 status: type: string enum: @@ -1479,29 +1483,67 @@ paths: type: array description: Valid list of callback URIs. minItems: 1 + maxItems: 5 + uniqueItems: true items: type: string format: uri userClaims: type: array - description: Allowed user info claims. + description: Allowed user info claims that can be requested by OIDC client. + minItems: 1 + maxItems: 30 + uniqueItems: true items: type: string + enum: + - name + - given_name + - middle_name + - preferred_username + - nickname + - gender + - birthdate + - email + - phone_number + - picture + - address authContextRefs: type: array description: Authentication Context Class Reference values. + minItems: 1 + maxItems: 30 + uniqueItems: true items: type: string + enum: + - 'mosip:idp:acr:static-code' + - 'mosip:idp:acr:generated-code' + - 'mosip:idp:acr:linked-wallet' + - 'mosip:idp:acr:biometrics' + - 'mosip:idp:acr:knowledge' + - 'mosip:idp:acr:password' + - 'mosip:idp:acr:id-token' grantTypes: type: array - description: Form of Authorization Grant. + description: Form of Authorization Grant presented to token endpoint. + minItems: 1 + maxItems: 3 + uniqueItems: true items: type: string + enum: + - authorization_code clientAuthMethods: type: array - description: Auth method supported for token endpoint. + description: Auth method supported for token endpoint. At present only "private_key_jwt" is supported. + minItems: 1 + maxItems: 3 + uniqueItems: true items: type: string + enum: + - private_key_jwt additionalConfig: type: object description: Additional configuration for the client. @@ -1509,7 +1551,7 @@ paths: type: object nullable: true description: |- - Encryption public key in JWK format. + Encryption public key in JWK format for userinfo JWE encryption. - Set to a valid JWK object to update - Set to null to clear the encryption key - Omit to leave unchanged @@ -1517,21 +1559,33 @@ paths: kty: type: string description: Key type (RSA or EC) + enum: + - RSA + - EC n: type: string description: RSA modulus (for RSA keys) e: type: string description: RSA exponent (for RSA keys) + enum: + - AQAB use: type: string description: Key use (enc for encryption) + enum: + - enc alg: type: string - description: Algorithm + description: Algorithm for key management + enum: + - RSA-OAEP-256 + - RSA-OAEP kid: type: string description: Key ID + required: + - kty examples: Update Status Only: value: From 6e0133d8d67c32301c35b718678ba60a6d637fad Mon Sep 17 00:00:00 2001 From: Md-Humair-KK Date: Thu, 5 Feb 2026 00:43:09 +0530 Subject: [PATCH 09/14] reloved conflicts Signed-off-by: Md-Humair-KK --- docs/esignet-openapi.yaml | 158 ++++++++++++++---- .../eSignet.postman_collection.json | 2 - 2 files changed, 123 insertions(+), 37 deletions(-) diff --git a/docs/esignet-openapi.yaml b/docs/esignet-openapi.yaml index 4d8d7fa7a..b96247b2a 100644 --- a/docs/esignet-openapi.yaml +++ b/docs/esignet-openapi.yaml @@ -1546,7 +1546,32 @@ paths: - private_key_jwt additionalConfig: type: object - description: Additional configuration for the client. + description: 'This parameter allow us to configure the required values based on their specific authentication and integration needs, ensuring efficient implementation of eSignet for ID verification/authentication.' + properties: + userinfo_response_type: + type: string + enum: + - JWS + - JWE + description: The response type for the user info endpoint should be configurable to allow the Relying Party to choose between only signed tokens or signed tokens with encryption. + purpose: + $ref: '#/components/schemas/Purpose' + signup_banner_required: + type: boolean + description: Relying Parties should be able to specify whether they require eSignet sign-up. If no signup service is required UI should not have "Sign up with Unified login" option. + forgot_pwd_link_required: + type: boolean + description: Relying Parties should be able to specify whether they require eSignet forgot password feature. If it is not required UI should not have "Forgot password" option. + consent_expire_in_mins: + type: number + minimum: 10 + description: The number of minutes after which a user's given consent will expire. + require_pushed_authorization_requests: + type: boolean + description: 'Boolean parameter indicating whether the only means of initiating an authorization request the client is allowed to use is PAR. If omitted, the default value is false.' + dpop_bound_access_tokens: + type: boolean + description: 'A boolean value specifying whether the client always uses DPoP for token requests. If omitted, the default value is false. If the value is true, the eSignet rejects token request from the client that do not contain the DPoP header.' encPublicKey: type: object nullable: true @@ -1555,54 +1580,117 @@ paths: - Set to a valid JWK object to update - Set to null to clear the encryption key - Omit to leave unchanged - properties: - kty: - type: string - description: Key type (RSA or EC) - enum: - - RSA - - EC - n: - type: string - description: RSA modulus (for RSA keys) - e: - type: string - description: RSA exponent (for RSA keys) - enum: - - AQAB - use: - type: string - description: Key use (enc for encryption) - enum: - - enc - alg: - type: string - description: Algorithm for key management - enum: - - RSA-OAEP-256 - - RSA-OAEP - kid: - type: string - description: Key ID - required: - - kty + oneOf: + - title: RSA Encryption Key + type: object + description: RSA public key for encryption + properties: + kty: + type: string + description: Key type (RSA) + enum: + - RSA + n: + type: string + description: RSA modulus (Base64URL encoded) + minLength: 1 + e: + type: string + description: RSA exponent + enum: + - AQAB + use: + type: string + description: Key use (enc for encryption) + enum: + - enc + alg: + type: string + description: Algorithm for key management + enum: + - RSA-OAEP-256 + - RSA-OAEP + kid: + type: string + description: Key ID + required: + - kty + - n + - e + - title: EC Encryption Key + type: object + description: Elliptic Curve public key for encryption + properties: + kty: + type: string + description: Key type (EC) + enum: + - EC + crv: + type: string + description: Curve name + enum: + - P-256 + - P-384 + - P-521 + x: + type: string + description: X coordinate (Base64URL encoded) + minLength: 1 + y: + type: string + description: Y coordinate (Base64URL encoded) + minLength: 1 + use: + type: string + description: Key use (enc for encryption) + enum: + - enc + alg: + type: string + description: Algorithm for key management + enum: + - ECDH-ES + - ECDH-ES+A128KW + - ECDH-ES+A192KW + - ECDH-ES+A256KW + kid: + type: string + description: Key ID + required: + - kty + - crv + - x + - y examples: Update Status Only: value: requestTime: '2024-01-15T10:30:00.000Z' request: status: INACTIVE - Update Encryption Key: + Update RSA Encryption Key: value: requestTime: '2024-01-15T10:30:00.000Z' request: encPublicKey: kty: RSA - n: 0vx7agoebGcQSuuPiLJXZptN... + n: 0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw e: AQAB use: enc alg: RSA-OAEP-256 - kid: enc-key-1 + kid: enc-key-rsa-1 + Update EC Encryption Key: + value: + requestTime: '2024-01-15T10:30:00.000Z' + request: + encPublicKey: + kty: EC + crv: P-256 + x: WbbkPq9chKwjFGa9U7CmEEIb3FJ0yHEaGrMsC3E6iKA + y: 8NKNqnR7v29K7NsMFhV9C0e-RYlBN0tA7qhwj4ZjxpU + use: enc + alg: ECDH-ES+A256KW + kid: enc-key-ec-1 Clear Encryption Key: value: requestTime: '2024-01-15T10:30:00.000Z' diff --git a/postman-collection/eSignet.postman_collection.json b/postman-collection/eSignet.postman_collection.json index cbb4acd5f..a0de2ed2e 100644 --- a/postman-collection/eSignet.postman_collection.json +++ b/postman-collection/eSignet.postman_collection.json @@ -271,8 +271,6 @@ "kp = pmlib.rs.KEYUTIL.generateKeypair(\"RSA\", 2048);", "privateKey_jwk = pmlib.rs.KEYUTIL.getJWK(kp.prvKeyObj);", "publicKey_jwk = pmlib.rs.KEYUTIL.getJWK(kp.pubKeyObj);", - "publicKey_jwk.alg = \"RS256\"; // Or your preferred algorithm", - "publicKey_jwk.use = \"sig\"; // Usually 'sig' for JWTs", "", "// Add alg and use values to client public key", "publicKey_jwk.alg = \"RS256\";", From 57a802d2d3191f86b3e1b4e0ad01029c539d14d7 Mon Sep 17 00:00:00 2001 From: Md-Humair-KK Date: Thu, 5 Feb 2026 11:28:08 +0530 Subject: [PATCH 10/14] updated field validations Signed-off-by: Md-Humair-KK --- .../mosip/esignet/core/dto/ClientDetailPatchRequest.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/esignet-core/src/main/java/io/mosip/esignet/core/dto/ClientDetailPatchRequest.java b/esignet-core/src/main/java/io/mosip/esignet/core/dto/ClientDetailPatchRequest.java index 0111e7c51..c79aad1ae 100644 --- a/esignet-core/src/main/java/io/mosip/esignet/core/dto/ClientDetailPatchRequest.java +++ b/esignet-core/src/main/java/io/mosip/esignet/core/dto/ClientDetailPatchRequest.java @@ -20,6 +20,7 @@ /** * DTO for PATCH client request. Independent class with all updatable fields. * All fields are optional - only provided fields will be updated. + * Null fields indicate "do not update this field". * Note: Client ID is immutable and cannot be updated. */ @Data @@ -31,8 +32,7 @@ public class ClientDetailPatchRequest { private String logoUri; @Size(message = ErrorConstants.INVALID_REDIRECT_URI, min = 1, max = 5) - private List<@NotBlank(message = ErrorConstants.INVALID_REDIRECT_URI) - @RedirectURL String> redirectUris; + private List<@RedirectURL String> redirectUris; @Size(message = ErrorConstants.INVALID_CLAIM, min = 1, max = 30) private List<@OIDCClaim String> userClaims; @@ -53,8 +53,7 @@ public class ClientDetailPatchRequest { private List<@OIDCClientAuth String> clientAuthMethods; private Map<@ClientNameLang String, - @NotBlank(message = ErrorConstants.INVALID_CLIENT_NAME_MAP_VALUE) @Size(max = 50, - message = ErrorConstants.INVALID_CLIENT_NAME_LENGTH) String> clientNameLangMap; + @Size(max = 50, message = ErrorConstants.INVALID_CLIENT_NAME_LENGTH) String> clientNameLangMap; @ClientAdditionalConfig private JsonNode additionalConfig; From 3342e16b11ffa9d1b7c80da953bfd657eb3506f2 Mon Sep 17 00:00:00 2001 From: Md-Humair-KK Date: Thu, 5 Feb 2026 11:32:20 +0530 Subject: [PATCH 11/14] updated open api yaml Signed-off-by: Md-Humair-KK --- docs/esignet-openapi.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/esignet-openapi.yaml b/docs/esignet-openapi.yaml index b96247b2a..de9a8fec5 100644 --- a/docs/esignet-openapi.yaml +++ b/docs/esignet-openapi.yaml @@ -1424,7 +1424,7 @@ paths: description: |- API to partially update existing OAuth/Open ID Connect (OIDC) client. Only provided fields will be updated. - **Special handling for enc_public_key:** + **Special handling for encPublicKey:** - When set/updated: validates format and computes enc_public_key_hash - When explicitly set to null: clears both enc_public_key and enc_public_key_hash - When not present in request: leaves both fields unchanged From 2a6e3dfb8ea70756f9c26bcc811ddac3271685a9 Mon Sep 17 00:00:00 2001 From: Md-Humair-KK Date: Thu, 5 Feb 2026 11:46:58 +0530 Subject: [PATCH 12/14] updated open api yaml Signed-off-by: Md-Humair-KK --- docs/esignet-openapi.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/esignet-openapi.yaml b/docs/esignet-openapi.yaml index de9a8fec5..1afbb95f3 100644 --- a/docs/esignet-openapi.yaml +++ b/docs/esignet-openapi.yaml @@ -1581,6 +1581,7 @@ paths: - Set to null to clear the encryption key - Omit to leave unchanged oneOf: + - type: 'null' - title: RSA Encryption Key type: object description: RSA public key for encryption From 27126dbd472da9d0b8814a431fcb834cfab16808 Mon Sep 17 00:00:00 2001 From: Md-Humair-KK Date: Thu, 5 Feb 2026 15:37:33 +0530 Subject: [PATCH 13/14] resolved review comments Signed-off-by: Md-Humair-KK --- .../services/ClientManagementServiceImpl.java | 13 ++++++++----- .../esignet/core/dto/ClientDetailPatchRequest.java | 7 ++++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/client-management-service-impl/src/main/java/io/mosip/esignet/services/ClientManagementServiceImpl.java b/client-management-service-impl/src/main/java/io/mosip/esignet/services/ClientManagementServiceImpl.java index 9455f543e..4a0559a7c 100644 --- a/client-management-service-impl/src/main/java/io/mosip/esignet/services/ClientManagementServiceImpl.java +++ b/client-management-service-impl/src/main/java/io/mosip/esignet/services/ClientManagementServiceImpl.java @@ -377,9 +377,7 @@ public ClientDetail buildClient(String clientId, ClientDetailPatchRequest patchR String existingName = clientDetail.getName(); Map existingNameMap = new HashMap<>(); try { - if (existingName != null) { - existingNameMap = objectMapper.readValue(existingName, new TypeReference>() {}); - } + existingNameMap = objectMapper.readValue(existingName, new TypeReference<>() {}); } catch (Exception e) { log.warn("Failed to parse existing client name as JSON, using empty map"); } @@ -400,8 +398,13 @@ public ClientDetail buildClient(String clientId, ClientDetailPatchRequest patchR } // Handle enc_public_key update - only if provided and not empty - if (patchRequest.getEncPublicKey() != null && !patchRequest.getEncPublicKey().isEmpty()) { - clientDetail.setEncPublicKey(IdentityProviderUtil.getJWKString(patchRequest.getEncPublicKey())); + if (patchRequest.getEncPublicKey() != null) { + try { + clientDetail.setEncPublicKey(IdentityProviderUtil.getJWKString(patchRequest.getEncPublicKey())); + } catch (EsignetException e) { + log.error("Invalid encryption public key",e); + throw e; + } clientDetail.setEncPublicKeyHash(identityProviderUtil.computePublicKeyHash(patchRequest.getEncPublicKey())); } diff --git a/esignet-core/src/main/java/io/mosip/esignet/core/dto/ClientDetailPatchRequest.java b/esignet-core/src/main/java/io/mosip/esignet/core/dto/ClientDetailPatchRequest.java index c79aad1ae..aabc215d5 100644 --- a/esignet-core/src/main/java/io/mosip/esignet/core/dto/ClientDetailPatchRequest.java +++ b/esignet-core/src/main/java/io/mosip/esignet/core/dto/ClientDetailPatchRequest.java @@ -20,7 +20,6 @@ /** * DTO for PATCH client request. Independent class with all updatable fields. * All fields are optional - only provided fields will be updated. - * Null fields indicate "do not update this field". * Note: Client ID is immutable and cannot be updated. */ @Data @@ -32,7 +31,8 @@ public class ClientDetailPatchRequest { private String logoUri; @Size(message = ErrorConstants.INVALID_REDIRECT_URI, min = 1, max = 5) - private List<@RedirectURL String> redirectUris; + private List<@NotBlank(message = ErrorConstants.INVALID_REDIRECT_URI) + @RedirectURL String> redirectUris; @Size(message = ErrorConstants.INVALID_CLAIM, min = 1, max = 30) private List<@OIDCClaim String> userClaims; @@ -53,7 +53,8 @@ public class ClientDetailPatchRequest { private List<@OIDCClientAuth String> clientAuthMethods; private Map<@ClientNameLang String, - @Size(max = 50, message = ErrorConstants.INVALID_CLIENT_NAME_LENGTH) String> clientNameLangMap; + @NotBlank(message = ErrorConstants.INVALID_CLIENT_NAME_MAP_VALUE) @Size(max = 50, + message = ErrorConstants.INVALID_CLIENT_NAME_LENGTH) String> clientNameLangMap; @ClientAdditionalConfig private JsonNode additionalConfig; From 777ed464f5bd7839bac5b6750c9b3b484e4b7eed Mon Sep 17 00:00:00 2001 From: Md-Humair-KK Date: Thu, 5 Feb 2026 22:54:13 +0530 Subject: [PATCH 14/14] fixed test case failure Signed-off-by: Md-Humair-KK --- .../esignet/ClientManagementServiceTest.java | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/client-management-service-impl/src/test/java/io/mosip/esignet/ClientManagementServiceTest.java b/client-management-service-impl/src/test/java/io/mosip/esignet/ClientManagementServiceTest.java index c9a73b740..6089a33b8 100644 --- a/client-management-service-impl/src/test/java/io/mosip/esignet/ClientManagementServiceTest.java +++ b/client-management-service-impl/src/test/java/io/mosip/esignet/ClientManagementServiceTest.java @@ -403,22 +403,18 @@ public void patchClient_withEncPublicKey_thenPass() throws EsignetException { } @Test - public void patchClient_withEmptyEncPublicKey_thenNoChange() throws EsignetException { + public void patchClient_withEmptyEncPublicKey_thenFail() { ClientDetail clientDetail = createMockClientDetail("client_id_v1"); Mockito.when(clientDetailRepository.findById("client_id_v1")).thenReturn(Optional.of(clientDetail)); ClientDetailPatchRequest patchRequest = new ClientDetailPatchRequest(); - patchRequest.setEncPublicKey(new HashMap<>()); + patchRequest.setEncPublicKey(new HashMap<>()); // Empty map - should throw exception - ClientDetail savedEntity = new ClientDetail(); - savedEntity.setId("client_id_v1"); - savedEntity.setStatus("ACTIVE"); - Mockito.when(clientDetailRepository.save(Mockito.any(ClientDetail.class))).thenReturn(savedEntity); + EsignetException exception = Assertions.assertThrows(EsignetException.class, () -> + clientManagementService.patchClient("client_id_v1", patchRequest) + ); - ClientDetailResponse response = clientManagementService.patchClient("client_id_v1", patchRequest); - - Assertions.assertNotNull(response); - Mockito.verify(identityProviderUtil, Mockito.never()).computePublicKeyHash(Mockito.any()); + Assertions.assertEquals(ErrorConstants.INVALID_PUBLIC_KEY, exception.getErrorCode()); } @Test