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 c3b11b943..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 @@ -322,4 +322,106 @@ public ClientDetail buildClient(String clientId, ClientDetailUpdateRequestV3 cli clientDetail.setAdditionalConfig(clientDetailUpdateRequestV3.getAdditionalConfig()); return clientDetail; } + + /** + * Build client detail entity for PATCH update operation. + * @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 { + 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) { + 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())); + } + + 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/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 da7a9a994..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 @@ -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,393 @@ 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(); + } 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()); + + Mockito.verify(identityProviderUtil, Mockito.times(1)).computePublicKeyHash(Mockito.any()); + } + + @Test + 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<>()); // Empty map - should throw exception + + EsignetException exception = Assertions.assertThrows(EsignetException.class, () -> + clientManagementService.patchClient("client_id_v1", patchRequest) + ); + + Assertions.assertEquals(ErrorConstants.INVALID_PUBLIC_KEY, exception.getErrorCode()); + } + + @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); + 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"); + + 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 +745,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..1afbb95f3 100644 --- a/docs/esignet-openapi.yaml +++ b/docs/esignet-openapi.yaml @@ -1417,6 +1417,343 @@ 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 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 + + **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. + additionalProperties: + type: string + minLength: 1 + maxLength: 50 + 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 + maxItems: 5 + uniqueItems: true + items: + type: string + format: uri + userClaims: + type: array + 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 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. 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: '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 + description: |- + 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 + oneOf: + - type: 'null' + - 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 RSA Encryption Key: + value: + requestTime: '2024-01-15T10:30:00.000Z' + request: + encPublicKey: + kty: RSA + n: 0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw + e: AQAB + use: enc + alg: RSA-OAEP-256 + 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' + 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..aabc215d5 --- /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..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 @@ -84,4 +84,13 @@ public interface ClientManagementService { */ ClientDetailResponse updateOAuthClient(String clientId, ClientDetailUpdateRequestV2 clientDetailUpdateRequestV2) throws EsignetException; + /** + * API to partially update (PATCH) registered relying party client. + * @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..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 @@ -157,4 +157,25 @@ public ResponseWrapper updateClientV2(@Valid @PathVariab return response; } + /** + * PATCH endpoint to partially update client details. + * @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..a0de2ed2e 100644 --- a/postman-collection/eSignet.postman_collection.json +++ b/postman-collection/eSignet.postman_collection.json @@ -271,8 +271,10 @@ "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\";", + "publicKey_jwk.use = \"sig\";", "", "// Get the public key in PEM format", "const publicKeyPem = pmlib.rs.KEYUTIL.getPEM(kp.pubKeyObj, \"PKCS8PUB\");", @@ -371,6 +373,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": [] } ] }