diff --git a/mock-db-plugin/README.md b/mock-db-plugin/README.md new file mode 100644 index 00000000..67b9aeb9 --- /dev/null +++ b/mock-db-plugin/README.md @@ -0,0 +1,51 @@ +# Mock DB plugin + +## About + +Implementation for all the interfaces defined in esignet-integration-api. Mock DB plugin is built to use eSignet with any database + +This library should be added as a runtime dependency to [esignet-service](https://github.com/mosip/esignet) for development purpose only. + +**Note**: This is not production use implementation. + +## Configurations + +Refer [application.properties](src/main/resources/application.properties) for all the configurations required to use this plugin implementation. All the properties +are set with default values. If required values can be overridden in the host application by setting them as environment variable. Refer [esignet-service](https://github.com/mosip/esignet) +docker-compose file to see how the configuration property values can be changed. + +Add "bindingtransaction" cache name in "mosip.esignet.cache.names" property. + +## Databases +You have to create a new database, table and some user details entries as well. + +``` +-- Step 1: Create Database +CREATE DATABASE IF NOT EXISTS mock_db; + +-- Step 2: Create User and Grant Privileges +CREATE USER IF NOT EXISTS 'mock_user'@'localhost' IDENTIFIED BY 'SecureP@ss123'; +GRANT ALL PRIVILEGES ON mock_db.* TO 'mock_user'@'localhost'; +FLUSH PRIVILEGES; + +-- Step 3: Use the Database +USE mock_db; + +-- Step 4: Create Table user_detail +CREATE TABLE IF NOT EXISTS user_detail ( + id VARCHAR(12) PRIMARY KEY, + name VARCHAR(100) NOT NULL, + dob DATE NOT NULL, + email VARCHAR(100) UNIQUE NOT NULL +); + +-- Step 5: Insert Sample Data +INSERT INTO user_detail (id, name, dob, email) VALUES +('3453434553', 'Alice Johnson', '1990-05-14', 'alice@example.com'), +('2583148061', 'Bob Smith', '1985-09-23', 'bob@example.com'), +('9834544352', 'Charlie Brown', '1992-07-11', 'charlie@example.com'), +('5236574533', 'Diana Ross', '1988-12-30', 'diana@example.com'); +``` + +## License +This project is licensed under the terms of [Mozilla Public License 2.0](LICENSE). diff --git a/mock-db-plugin/pom.xml b/mock-db-plugin/pom.xml new file mode 100644 index 00000000..22b24b2f --- /dev/null +++ b/mock-db-plugin/pom.xml @@ -0,0 +1,79 @@ + + + 4.0.0 + + org.mock.esignet.plugin + mock-db-plugin + 1.0-SNAPSHOT + + + 11 + 11 + UTF-8 + + + + + io.mosip.esignet + esignet-integration-api + 1.5.1 + provided + + + + io.mosip.esignet + esignet-core + 1.5.1 + provided + + + + com.mysql + mysql-connector-j + 8.0.33 + + + + org.projectlombok + lombok + 1.18.36 + provided + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.3.0 + + + jar-with-dependencies + + false + + + + make-assembly + package + + single + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 9 + 9 + + + + + + \ No newline at end of file diff --git a/mock-db-plugin/src/main/java/org/mock/esignet/plugin/config/MockDBConfig.java b/mock-db-plugin/src/main/java/org/mock/esignet/plugin/config/MockDBConfig.java new file mode 100644 index 00000000..2dd142dc --- /dev/null +++ b/mock-db-plugin/src/main/java/org/mock/esignet/plugin/config/MockDBConfig.java @@ -0,0 +1,35 @@ +package org.mock.esignet.plugin.config; + +import com.zaxxer.hikari.HikariDataSource; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.JdbcTemplate; + +import javax.sql.DataSource; + +@Configuration +public class MockDBConfig { + @Value("${org.mock.esignet.plugin.db-url}") + private String dbURL; + + @Value("${org.mock.esignet.plugin.db-username}") + private String dbUsername; + + @Value("${org.mock.esignet.plugin.db-password}") + private String dbPassword; + + public DataSource dataSource() { + HikariDataSource hikariDataSource = new HikariDataSource(); + hikariDataSource.setJdbcUrl(dbURL); + hikariDataSource.setUsername(dbUsername); + hikariDataSource.setPassword(dbPassword); + hikariDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); + return hikariDataSource; + } + + @Bean("mockPluginJdbcTemplate") + public JdbcTemplate mockPluginJdbcTemplate() { + return new JdbcTemplate(dataSource()); + } +} diff --git a/mock-db-plugin/src/main/java/org/mock/esignet/plugin/dto/UserDetail.java b/mock-db-plugin/src/main/java/org/mock/esignet/plugin/dto/UserDetail.java new file mode 100644 index 00000000..a9152cf0 --- /dev/null +++ b/mock-db-plugin/src/main/java/org/mock/esignet/plugin/dto/UserDetail.java @@ -0,0 +1,13 @@ +package org.mock.esignet.plugin.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@AllArgsConstructor +@Data +public class UserDetail { + private String id; + private String name; + private String dob; + private String email; +} diff --git a/mock-db-plugin/src/main/java/org/mock/esignet/plugin/repositories/UserDetailRepository.java b/mock-db-plugin/src/main/java/org/mock/esignet/plugin/repositories/UserDetailRepository.java new file mode 100644 index 00000000..5b5a0161 --- /dev/null +++ b/mock-db-plugin/src/main/java/org/mock/esignet/plugin/repositories/UserDetailRepository.java @@ -0,0 +1,34 @@ +package org.mock.esignet.plugin.repositories; + +import org.mock.esignet.plugin.dto.UserDetail; +import org.springframework.stereotype.Repository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +@Repository +public class UserDetailRepository { + private static final String QUERY = "select * from user_detail where id=?"; + + @Autowired + @Qualifier("mockPluginJdbcTemplate") + private JdbcTemplate mockPluginJdbcTemplate; + + public UserDetail findUserById(String individualId) { + List list = mockPluginJdbcTemplate.query(QUERY, new Object[]{individualId}, new RowMapper() { + @Override + public UserDetail mapRow(ResultSet resultSet, int i) throws SQLException { + return new UserDetail(resultSet.getString(1),resultSet.getString(2), resultSet.getString(3), resultSet.getString(4)); + } + }); + + if(!list.isEmpty()) { + return list.get(0); + } + return null; + } +} diff --git a/mock-db-plugin/src/main/java/org/mock/esignet/plugin/service/MockDBAuthenticatorImpl.java b/mock-db-plugin/src/main/java/org/mock/esignet/plugin/service/MockDBAuthenticatorImpl.java new file mode 100644 index 00000000..60c04267 --- /dev/null +++ b/mock-db-plugin/src/main/java/org/mock/esignet/plugin/service/MockDBAuthenticatorImpl.java @@ -0,0 +1,178 @@ +package org.mock.esignet.plugin.service; + +import io.mosip.esignet.api.dto.*; +import io.mosip.esignet.api.exception.KycAuthException; +import io.mosip.esignet.api.exception.KycExchangeException; +import io.mosip.esignet.api.exception.KycSigningCertificateException; +import io.mosip.esignet.api.exception.SendOtpException; +import io.mosip.esignet.api.spi.Authenticator; + +import java.util.List; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import org.bouncycastle.x509.X509V3CertificateGenerator; +import org.mock.esignet.plugin.dto.UserDetail; +import org.mock.esignet.plugin.repositories.UserDetailRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.security.auth.x500.X500Principal; +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPrivateKey; +import java.time.ZoneOffset; +import java.util.*; + +@Component +public class MockDBAuthenticatorImpl implements Authenticator { + + private Map localMap = new HashMap<>(); + + @Autowired + private UserDetailRepository userDetailRepository; + private X509Certificate keyCertificate; + private KeyPair localKey; + + @Override + public KycAuthResult doKycAuth(String relyingPartyId, String clientId, KycAuthDto kycAuthDto) throws KycAuthException { + UserDetail userDetail = userDetailRepository.findUserById(kycAuthDto.getIndividualId()); + + if (userDetail == null) + throw new KycAuthException("user_not_found"); + + boolean authStatus = false; + AuthChallenge authChallenge = kycAuthDto.getChallengeList().get(0); + switch (authChallenge.getAuthFactorType()) { + case "OTP": + authStatus = authChallenge.getChallenge().equals("111111"); + break; + default: + throw new KycAuthException("invalid_auth_factor"); + } + + if(!authStatus) + throw new KycAuthException("auth_failed"); + + String token = UUID.randomUUID().toString(); + localMap.put(token, kycAuthDto.getIndividualId()); + KycAuthResult kycAuthResult = new KycAuthResult(); + kycAuthResult.setKycToken(token); + kycAuthResult.setPartnerSpecificUserToken(token); + return kycAuthResult; + } + + @Override + public KycExchangeResult doKycExchange(String relyingPartyId, String clientId, KycExchangeDto kycExchangeDto) throws KycExchangeException { + String storedKycToken = localMap.get(kycExchangeDto.getKycToken()); + if (storedKycToken == null) + throw new KycExchangeException("invalid_kyc_token"); + + UserDetail userDetail = userDetailRepository.findUserById(storedKycToken); + + JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder() + .subject(storedKycToken) + .issuer("eSignet") + .expirationTime(new Date(System.currentTimeMillis() + 3600 * 1000)); + + for (String claim : kycExchangeDto.getAcceptedClaims()) { + switch (claim) { + case "name": + builder.claim("name", userDetail.getName()); + break; + case "birthdate": + builder.claim("birthDate", userDetail.getDob()); + break; + case "email": + builder.claim("email", userDetail.getEmail()); + break; + } + } + + JWTClaimsSet claimsSet = builder.build(); + String signedJWT = signJWT(claimsSet); //additionally can be encrypted with public key of relying party + + KycExchangeResult kycExchangeResult = new KycExchangeResult(); + kycExchangeResult.setEncryptedKyc(signedJWT); + return kycExchangeResult; + } + + + + @Override + public SendOtpResult sendOtp(String relyingPartyId, String clientId, SendOtpDto sendOtpDto) throws SendOtpException { + SendOtpResult sendOtpResult = new SendOtpResult(); + sendOtpResult.setTransactionId(sendOtpDto.getTransactionId()); + sendOtpResult.setMaskedMobile(""); + return sendOtpResult; + } + + @Override + public boolean isSupportedOtpChannel(String channel) { + return true; + } + + @Override + public List getAllKycSigningCertificates() throws KycSigningCertificateException { + KycSigningCertificateData kycSigningCertificateData = new KycSigningCertificateData(); + try { + + Base64.Encoder encoder = Base64.getMimeEncoder(64, "\n".getBytes()); + String encodedCert = encoder.encodeToString(keyCertificate.getEncoded()); + kycSigningCertificateData.setCertificateData("-----BEGIN CERTIFICATE-----\n" + encodedCert + "\n-----END CERTIFICATE-----"); + kycSigningCertificateData.setExpiryAt(keyCertificate.getNotAfter().toInstant().atZone(ZoneOffset.UTC).toLocalDateTime()); + kycSigningCertificateData.setIssuedAt(keyCertificate.getNotBefore().toInstant().atZone(ZoneOffset.UTC).toLocalDateTime()); + + } catch (CertificateEncodingException e) { + throw new RuntimeException(e); + } + //return public key from this method + return List.of(kycSigningCertificateData); + } + + public void generateKeyCertificate() { + if (localKey == null || keyCertificate == null) { + try { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + localKey = keyPairGenerator.generateKeyPair(); + + X509V3CertificateGenerator certGen = new X509V3CertificateGenerator(); + X500Principal dnName = new X500Principal("CN=Self-Signed, O=Example Org, C=US"); + certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis())); + certGen.setSubjectDN(dnName); + certGen.setIssuerDN(dnName); // Self-signed + certGen.setNotBefore(new Date(System.currentTimeMillis())); + certGen.setNotAfter(new Date(System.currentTimeMillis() + (365 * 24 * 60 * 60 * 1000L))); // 1 year validity + certGen.setPublicKey(localKey.getPublic()); + certGen.setSignatureAlgorithm("SHA256WithRSAEncryption"); + keyCertificate = certGen.generate(localKey.getPrivate(), "BC"); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + private String signJWT(JWTClaimsSet claimsSet) throws KycExchangeException { + if(localKey == null || keyCertificate == null) { + generateKeyCertificate(); + } + + JWSHeader header = new JWSHeader(JWSAlgorithm.RS256); + SignedJWT signedJWT = new SignedJWT(header, claimsSet); + JWSSigner signer = new RSASSASigner((RSAPrivateKey)localKey.getPrivate()); + try { + signedJWT.sign(signer); + } catch (JOSEException e) { + throw new KycExchangeException("signing_failed"); + } + return signedJWT.serialize(); + } +} diff --git a/mock-db-plugin/src/main/resources/application.properties b/mock-db-plugin/src/main/resources/application.properties new file mode 100644 index 00000000..ba03f24c --- /dev/null +++ b/mock-db-plugin/src/main/resources/application.properties @@ -0,0 +1,15 @@ +## eSignet mock plugin configuration +mosip.esignet.integration.scan-base-package=org.mock.esignet.plugin +mosip.esignet.integration.authenticator=MockDBAuthenticatorImpl +mosip.esignet.integration.key-binder=NoOpKeyBinder + +# MySQL (Plugin DataSource) +org.mock.esignet.plugin.db-url=jdbc:mysql://localhost:3306/mock_db?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC +org.mock.esignet.plugin.db-username=mock_user +org.mock.esignet.plugin.db-password=SecureP@ss123 +spring.datasource.plugin.driver-class-name=com.mysql.cj.jdbc.Driver + +## Disable authz & authn +mosip.esignet.security.auth.post-urls={} +mosip.esignet.security.auth.put-urls={} +mosip.esignet.security.auth.get-urls={} \ No newline at end of file