diff --git a/.github/workflows/push-trigger.yml b/.github/workflows/push-trigger.yml index 0f46620..192180b 100644 --- a/.github/workflows/push-trigger.yml +++ b/.github/workflows/push-trigger.yml @@ -186,3 +186,44 @@ jobs: OSSRH_TOKEN: ${{ secrets.OSSRH_TOKEN }} GPG_SECRET: ${{ secrets.GPG_SECRET }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} + + build-maven-mock-ida-dataprovider-plugin: + uses: mosip/kattu/.github/workflows/maven-build.yml@master-java21 + with: + SERVICE_LOCATION: mock-ida-dataprovider-plugin + BUILD_ARTIFACT: mock-ida-dataprovider-plugin + secrets: + OSSRH_USER: ${{ secrets.OSSRH_USER }} + OSSRH_SECRET: ${{ secrets.OSSRH_SECRET }} + OSSRH_TOKEN: ${{ secrets.OSSRH_TOKEN }} + GPG_SECRET: ${{ secrets.GPG_SECRET }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} + + publish_to_nexus_mock-ida-dataprovider-plugin: + if: "${{ !contains(github.ref, 'master') && github.event_name != 'pull_request' && github.event_name != 'release' && github.event_name != 'prerelease' && github.event_name != 'publish' }}" + needs: build-maven-mock-ida-dataprovider-plugin + uses: mosip/kattu/.github/workflows/maven-publish-to-nexus.yml@master-java21 + with: + SERVICE_LOCATION: ./mock-ida-dataprovider-plugin + secrets: + OSSRH_USER: ${{ secrets.OSSRH_USER }} + OSSRH_SECRET: ${{ secrets.OSSRH_SECRET }} + OSSRH_URL: ${{ secrets.OSSRH_SNAPSHOT_URL }} + OSSRH_TOKEN: ${{ secrets.OSSRH_TOKEN }} + GPG_SECRET: ${{ secrets.GPG_SECRET }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} + + sonar_analysis_mock-ida-dataprovider-plugin: + needs: build-maven-mock-ida-dataprovider-plugin + if: "${{ github.event_name != 'pull_request' }}" + uses: mosip/kattu/.github/workflows/maven-sonar-analysis.yml@master-java21 + with: + SERVICE_LOCATION: ./mock-ida-dataprovider-plugin + secrets: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + ORG_KEY: ${{ secrets.ORG_KEY }} + OSSRH_USER: ${{ secrets.OSSRH_USER }} + OSSRH_SECRET: ${{ secrets.OSSRH_SECRET }} + OSSRH_TOKEN: ${{ secrets.OSSRH_TOKEN }} + GPG_SECRET: ${{ secrets.GPG_SECRET }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} diff --git a/mock-ida-dataprovider-plugin/pom.xml b/mock-ida-dataprovider-plugin/pom.xml new file mode 100644 index 0000000..4ba5aa8 --- /dev/null +++ b/mock-ida-dataprovider-plugin/pom.xml @@ -0,0 +1,353 @@ + + 4.0.0 + + io.mosip.certify + mock-ida-dataprovider-plugin + 0.3.0-SNAPSHOT + jar + + mock-ida-dataprovider-plugin + Mockup of a data provider plugin implementation that is used to showcase the integration with certify + https://github.com/mosip/digital-credential-plugins + + + + MPL 2.0 + https://www.mozilla.org/en-US/MPL/2.0/ + + + + scm:git:git://github.com/mosip/digital-credential-plugins.git + scm:git:ssh://github.com:mosip/digital-credential-plugins.git + https://github.com/mosip/digital-credential-plugins + HEAD + + + + Mosip + mosip.emailnotifier@gmail.com + io.mosip + https://www.mosip.io + + + + + UTF-8 + 21 + 3.7.1 + 21 + 21 + 3.10.1 + 3.2.5 + 1.5 + 2.2.1 + 6.1.0 + 3.0.1 + 0.8.11 + 3.6.3 + 1.3.0-beta.1 + + + + + junit + junit + 3.8.1 + test + + + org.projectlombok + lombok + 1.18.30 + provided + + + io.mosip.certify + certify-core + 0.10.0-SNAPSHOT + provided + + + io.mosip.esignet + esignet-core + 1.4.1 + + + * + * + + + + + io.mosip.esignet + esignet-integration-api + 1.4.1 + + + * + * + + + + + + io.mosip.kernel + kernel-keymanager-service + ${kernel-keymanager-service.version} + provided + lib + + + org.springframework.cloud + spring-cloud-starter-sleuth + + + org.springframework.security + spring-security-test + + + org.slf4j + slf4j-api + + + + + org.slf4j + slf4j-api + 2.0.12 + + + junit + junit + 4.13.1 + test + + + + + + ossrh + CentralRepository + https://oss.sonatype.org/content/repositories/snapshots + default + + true + + + + central + MavenCentral + default + https://repo1.maven.org/maven2 + + false + + + + danubetech-maven-public + https://repo.danubetech.com/repository/maven-public/ + + + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + ${maven-assembly-plugin.version} + + + jar-with-dependencies + + false + + + + make-assembly + package + + single + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + + attach-javadocs + + jar + + + + + none + src/main/java + + + + maven-deploy-plugin + 3.1.2 + + + default-deploy + deploy + + deploy + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.14 + true + + + default-deploy + deploy + + deploy + + + + + ossrh + https://oss.sonatype.org/ + false + + + + + org.apache.maven.plugins + maven-source-plugin + true + 2.2.1 + + + attach-sources + + jar-no-fork + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.5 + + + sign-artifacts + verify + + sign + + + + --pinentry-mode + loopback + + + + + + + pl.project13.maven + git-commit-id-plugin + 3.0.1 + + + get-the-git-infos + + revision + + validate + + + + true + ${project.build.outputDirectory}/git.properties + + ^git.build.(time|version)$ + ^git.commit.id.(abbrev|full)$ + + full + ${project.basedir}/.git + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + false + false + + ${argLine} --add-opens + java.xml/jdk.xml.internal=ALL-UNNAMED + --illegal-access=permit + + + + + org.jacoco + jacoco-maven-plugin + ${maven.jacoco.version} + + + + prepare-agent + + + + report + prepare-package + + report + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 3.1.0 + + + make-jar-executable + package + + run + + + + + + + + + + + + + \ No newline at end of file diff --git a/mock-ida-dataprovider-plugin/src/main/java/io/mosip/certify/mockidadataprovider/integration/service/MockIdaDataProviderPlugin.java b/mock-ida-dataprovider-plugin/src/main/java/io/mosip/certify/mockidadataprovider/integration/service/MockIdaDataProviderPlugin.java new file mode 100644 index 0000000..3ae8675 --- /dev/null +++ b/mock-ida-dataprovider-plugin/src/main/java/io/mosip/certify/mockidadataprovider/integration/service/MockIdaDataProviderPlugin.java @@ -0,0 +1,140 @@ +package io.mosip.certify.mockidadataprovider.integration.service; + + +import io.mosip.certify.api.exception.DataProviderExchangeException; +import io.mosip.certify.api.spi.DataProviderPlugin; +import io.mosip.certify.core.exception.CertifyException; +import io.mosip.esignet.core.dto.OIDCTransaction; +import io.mosip.kernel.core.keymanager.spi.KeyStore; +import io.mosip.kernel.keymanagerservice.constant.KeymanagerConstant; +import io.mosip.kernel.keymanagerservice.entity.KeyAlias; +import io.mosip.kernel.keymanagerservice.helper.KeymanagerDBHelper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import javax.crypto.Cipher; +import java.security.Key; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; + +@ConditionalOnProperty(value = "mosip.certify.integration.data-provider-plugin", havingValue = "MockIdaDataProviderPlugin") +@Component +@Slf4j +public class MockIdaDataProviderPlugin implements DataProviderPlugin { + private static final String AES_CIPHER_FAILED = "aes_cipher_failed"; + private static final String NO_UNIQUE_ALIAS = "no_unique_alias"; + + private static final String ACCESS_TOKEN_HASH = "accessTokenHash"; + + public static final String UTC_DATETIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; + + public static final String CERTIFY_SERVICE_APP_ID = "CERTIFY_SERVICE"; + + @Autowired + private RestTemplate restTemplate; + + @Autowired + private KeyStore keyStore; + + @Autowired + private KeymanagerDBHelper dbHelper; + + @Autowired + private MockTransactionHelper mockTransactionHelper; + + @Value("${mosip.certify.mock.authenticator.get-identity-url}") + private String getIdentityUrl; + + @Value("${mosip.certify.cache.security.secretkey.reference-id}") + private String cacheSecretKeyRefId; + + @Value("${mosip.certify.cache.security.algorithm-name}") + private String aesECBTransformation; + + @Value("${mosip.certify.cache.secure.individual-id}") + private boolean isIndividualIDEncrypted; + + @Value("${mosip.certify.cache.store.individual-id}") + private boolean storeIndividualId; + + @Override + public Map fetchData(Map identityDetails) throws DataProviderExchangeException { + try { + OIDCTransaction transaction = mockTransactionHelper.getUserInfoTransaction(identityDetails.get(ACCESS_TOKEN_HASH).toString()); + String individualId = getIndividualId(transaction); + if (individualId != null) { + Map res = restTemplate.getForObject( + getIdentityUrl + "/" + individualId, + HashMap.class); + res = (Map) res.get("response"); + Map ret = new HashMap<>(); + ret.put("vcVer", "VC-V1"); + ret.put("id", getIdentityUrl + "/" + individualId); + ret.put("UIN", individualId); + ret.put("fullName", res.get("fullName")); + ret.put("gender", res.get("gender")); + ret.put("dateOfBirth", res.get("dateOfBirth")); + ret.put("email", res.get("email")); + ret.put("phone", res.get("phone")); + ret.put("addressLine1", res.get("streetAddress")); + ret.put("province", res.get("locality")); + ret.put("region", res.get("region")); + ret.put("postalCode", res.get("postalCode")); + ret.put("face", res.get("encodedPhoto")); + return ret; + } + } catch (Exception e) { + log.error("Failed to fetch json data for from data provider plugin", e); + throw new DataProviderExchangeException("ERROR_FETCHING_IDENTITY_DATA"); + } + + throw new DataProviderExchangeException("INVALID_ACCESS_TOKEN"); + } + + protected String getIndividualId(OIDCTransaction transaction) { + if (!storeIndividualId) + return null; + return isIndividualIDEncrypted ? decryptIndividualId(transaction.getIndividualId()) : transaction.getIndividualId(); + } + + private String decryptIndividualId(String encryptedIndividualId) { + try { + Cipher cipher = Cipher.getInstance(aesECBTransformation); + byte[] decodedBytes = Base64.getUrlDecoder().decode(encryptedIndividualId); + cipher.init(Cipher.DECRYPT_MODE, getSecretKeyFromHSM()); + return new String(cipher.doFinal(decodedBytes, 0, decodedBytes.length)); + } catch (Exception e) { + log.error("Error Cipher Operations of provided secret data.", e); + throw new CertifyException(AES_CIPHER_FAILED); + } + } + + private Key getSecretKeyFromHSM() { + String keyAlias = getKeyAlias(CERTIFY_SERVICE_APP_ID, cacheSecretKeyRefId); + if (Objects.nonNull(keyAlias)) { + return keyStore.getSymmetricKey(keyAlias); + } + throw new CertifyException(NO_UNIQUE_ALIAS); + } + + private String getKeyAlias(String keyAppId, String keyRefId) { + Map> keyAliasMap = dbHelper.getKeyAliases(keyAppId, keyRefId, LocalDateTime.now(ZoneOffset.UTC)); + List currentKeyAliases = keyAliasMap.get(KeymanagerConstant.CURRENTKEYALIAS); + if (currentKeyAliases != null && currentKeyAliases.size() == 1) { + return currentKeyAliases.get(0).getAlias(); + } + log.error("CurrentKeyAlias is not unique. KeyAlias count: {}", currentKeyAliases.size()); + throw new CertifyException(NO_UNIQUE_ALIAS); + } + + private static String getUTCDateTime() { + return ZonedDateTime.now(ZoneOffset.UTC).format(DateTimeFormatter.ofPattern(UTC_DATETIME_PATTERN)); + } +} \ No newline at end of file diff --git a/mock-ida-dataprovider-plugin/src/main/java/io/mosip/certify/mockidadataprovider/integration/service/MockTransactionHelper.java b/mock-ida-dataprovider-plugin/src/main/java/io/mosip/certify/mockidadataprovider/integration/service/MockTransactionHelper.java new file mode 100644 index 0000000..bf52c42 --- /dev/null +++ b/mock-ida-dataprovider-plugin/src/main/java/io/mosip/certify/mockidadataprovider/integration/service/MockTransactionHelper.java @@ -0,0 +1,25 @@ +package io.mosip.certify.mockidadataprovider.integration.service; + +import io.mosip.certify.api.exception.DataProviderExchangeException; +import io.mosip.esignet.core.dto.OIDCTransaction; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Component; + +@Component +public class MockTransactionHelper { + @Value("${mosip.certify.mock.vci-user-info-cache:userinfo}") + private String userinfoCache; + + @Autowired + private CacheManager cacheManager; + + public OIDCTransaction getUserInfoTransaction(String accessTokenHash) throws DataProviderExchangeException { + if(cacheManager.getCache(userinfoCache) != null) { + return cacheManager.getCache(userinfoCache).get(accessTokenHash, OIDCTransaction.class); + } + + throw new DataProviderExchangeException("CACHE_MISSING"); + } +} diff --git a/mock-ida-dataprovider-plugin/src/test/java/io/mosip/certify/mockidadataprovider/integration/service/MockIdaDataProviderPluginTest.java b/mock-ida-dataprovider-plugin/src/test/java/io/mosip/certify/mockidadataprovider/integration/service/MockIdaDataProviderPluginTest.java new file mode 100644 index 0000000..4b7f199 --- /dev/null +++ b/mock-ida-dataprovider-plugin/src/test/java/io/mosip/certify/mockidadataprovider/integration/service/MockIdaDataProviderPluginTest.java @@ -0,0 +1,92 @@ +package io.mosip.certify.mockidadataprovider.integration.service; + +import io.mosip.certify.api.exception.DataProviderExchangeException; +import io.mosip.esignet.core.dto.OIDCTransaction; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.support.NoOpCache; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.client.RestTemplate; + +import java.util.HashMap; +import java.util.Map; + +@RunWith(MockitoJUnitRunner.class) +public class MockIdaDataProviderPluginTest { + @Mock + CacheManager cacheManager; + + @Mock + Cache cache=new NoOpCache("test"); + + @Mock + MockTransactionHelper mockTransactionHelper; + + @Mock + RestTemplate restTemplate; + + @InjectMocks + MockIdaDataProviderPlugin mockDataProviderPlugin; + + @Before + public void setup() throws DataProviderExchangeException { + ReflectionTestUtils.setField(mockDataProviderPlugin,"getIdentityUrl","http://example.com"); + ReflectionTestUtils.setField(mockDataProviderPlugin,"cacheSecretKeyRefId","cacheSecretKeyRefId"); + ReflectionTestUtils.setField(mockDataProviderPlugin,"aesECBTransformation","AES/ECB/PKCS5Padding"); + ReflectionTestUtils.setField(mockDataProviderPlugin,"storeIndividualId",true); + ReflectionTestUtils.setField(mockDataProviderPlugin,"isIndividualIDEncrypted",false); + + OIDCTransaction oidcTransaction = new OIDCTransaction(); + oidcTransaction.setTransactionId("test"); + oidcTransaction.setIndividualId("individualId"); + oidcTransaction.setKycToken("kycToken"); + oidcTransaction.setAuthTransactionId("authTransactionId"); + oidcTransaction.setRelyingPartyId("relyingPartyId"); + oidcTransaction.setClaimsLocales(new String[]{"en-US", "en", "en-CA", "fr-FR", "fr-CA"}); + Mockito.when(mockTransactionHelper.getUserInfoTransaction("ACCESS_TOKEN_HASH")).thenReturn(oidcTransaction); + + Map identityJson = new HashMap<>(); + identityJson.put("fullName", "fullName"); + identityJson.put("gender", "gender"); + identityJson.put("dateOfBirth", "dateOfBirth"); + identityJson.put("email", "email"); + identityJson.put("phone", "phone"); + identityJson.put("streetAddress", "streetAddress"); + identityJson.put("locality", "locality"); + identityJson.put("region", "region"); + identityJson.put("postalCode", "postalCode"); + identityJson.put("encodedPhoto", "encodedPhoto"); + Map response = new HashMap<>(); + response.put("response", identityJson); + Mockito.when(restTemplate.getForObject(Mockito.anyString(), Mockito.any())).thenReturn(response); + } + + @Test + public void getJSONDataWithValidDetails_thenPass() throws DataProviderExchangeException { + Map jsonData = mockDataProviderPlugin.fetchData(Map.of("accessTokenHash","ACCESS_TOKEN_HASH","client_id","CLIENT_ID")); + Assert.assertNotNull(jsonData); + Assert.assertNotNull(jsonData.get("fullName")); + Assert.assertEquals("fullName" ,jsonData.get("fullName")); + Assert.assertNotNull(jsonData.get("UIN")); + Assert.assertEquals("individualId", jsonData.get("UIN")); + Assert.assertNotNull(jsonData.get("id")); + Assert.assertEquals("http://example.com/individualId", jsonData.get("id")); + } + + @Test + public void getJSONDataWithInValidDetails_thenFail() { + try { + mockDataProviderPlugin.fetchData(Map.of("accessTokenHash","test","client_id","CLIENT_ID")); + } catch (DataProviderExchangeException e) { + Assert.assertEquals("ERROR_FETCHING_IDENTITY_DATA", e.getMessage()); + } + } +} \ No newline at end of file diff --git a/mock-ida-dataprovider-plugin/src/test/java/io/mosip/certify/mockidadataprovider/integration/service/MockTransactionHelperTest.java b/mock-ida-dataprovider-plugin/src/test/java/io/mosip/certify/mockidadataprovider/integration/service/MockTransactionHelperTest.java new file mode 100644 index 0000000..dac2b2e --- /dev/null +++ b/mock-ida-dataprovider-plugin/src/test/java/io/mosip/certify/mockidadataprovider/integration/service/MockTransactionHelperTest.java @@ -0,0 +1,57 @@ +package io.mosip.certify.mockidadataprovider.integration.service; + +import io.mosip.certify.api.exception.DataProviderExchangeException; +import io.mosip.esignet.core.dto.OIDCTransaction; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.support.NoOpCache; +import org.springframework.test.util.ReflectionTestUtils; + +@RunWith(MockitoJUnitRunner.class) +public class MockTransactionHelperTest { + @Mock + CacheManager cacheManager; + + @Mock + Cache cache=new NoOpCache("test"); + + @InjectMocks + MockTransactionHelper mockTransactionHelper; + + @Before + public void setup() { + ReflectionTestUtils.setField(mockTransactionHelper, "userinfoCache", "test"); + OIDCTransaction oidcTransaction = new OIDCTransaction(); + oidcTransaction.setTransactionId("test"); + oidcTransaction.setIndividualId("individualId"); + oidcTransaction.setKycToken("kycToken"); + oidcTransaction.setAuthTransactionId("authTransactionId"); + oidcTransaction.setRelyingPartyId("relyingPartyId"); + oidcTransaction.setClaimsLocales(new String[]{"en-US", "en", "en-CA", "fr-FR", "fr-CA"}); + + Mockito.when(cacheManager.getCache(Mockito.anyString())).thenReturn(cache); + Mockito.when(cache.get("test", OIDCTransaction.class)).thenReturn(oidcTransaction); + } + + @Test + public void getOIDCTransactionWithValidDetails_thenPass() throws DataProviderExchangeException { + OIDCTransaction transaction = mockTransactionHelper.getUserInfoTransaction("test"); + Assert.assertNotNull(transaction); + Assert.assertEquals("test", transaction.getTransactionId()); + Assert.assertEquals("individualId", transaction.getIndividualId()); + } + + @Test + public void getOIDCTransactionWithInValidUserinfo_thenFail() throws DataProviderExchangeException { + OIDCTransaction transaction = mockTransactionHelper.getUserInfoTransaction("ACCESS_TOKEN_HASH"); + Assert.assertNull(transaction); + } +}