From 2a46ad8d0014cb230ffa37898276065f69cfa401 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Mon, 17 Feb 2025 15:15:07 +0100 Subject: [PATCH 01/14] feature/Verify the certificate's trust chain up to a trusted certification authority --- .../code/api/util/CertificateVerifier.scala | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 obp-api/src/main/scala/code/api/util/CertificateVerifier.scala diff --git a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala new file mode 100644 index 000000000..59f9bab14 --- /dev/null +++ b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala @@ -0,0 +1,102 @@ +package code.api.util + +import java.io.ByteArrayInputStream +import java.security.KeyStore +import java.security.cert._ +import java.util.{Base64, Collections} +import javax.net.ssl.TrustManagerFactory +import scala.jdk.CollectionConverters._ +import scala.util.{Failure, Success, Try} + +object CertificateVerifier { + + def verifyCertificate(pemCertificate: String): Boolean = { + Try { + // Convert PEM string to X.509 Certificate + val certificate = parsePemToX509Certificate(pemCertificate) + + // Load the default trust store (can be replaced with a custom one) + val trustStore = KeyStore.getInstance(KeyStore.getDefaultType) + + + val trustStorePath = Option(System.getProperty("javax.net.ssl.trustStore")) + .getOrElse("/usr/lib/jvm/java-17-openjdk-amd64/lib/security/cacerts") + + val trustStoreInputStream = new java.io.FileInputStream(trustStorePath) + trustStore.load(trustStoreInputStream, "changeit".toCharArray) // Default password: changeit + trustStoreInputStream.close() + + + val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm) + // trustStore.load(null, null) // Load default trust store + trustManagerFactory.init(trustStore) + + // Get trusted CAs from the trust store + val trustAnchors = trustStore.aliases().asScala + .filter(trustStore.isCertificateEntry) + .map(alias => trustStore.getCertificate(alias).asInstanceOf[X509Certificate]) + .map(cert => new TrustAnchor(cert, null)) + .toSet + .asJava // Convert Scala Set to Java Set + + if (trustAnchors.isEmpty) throw new Exception("No trusted certificates found in trust store.") + + // Set up PKIX parameters for validation + val pkixParams = new PKIXParameters(trustAnchors) + pkixParams.setRevocationEnabled(false) // Disable CRL checks + + // Validate certificate chain + val certPath = CertificateFactory.getInstance("X.509").generateCertPath(Collections.singletonList(certificate)) + val validator = CertPathValidator.getInstance("PKIX") + validator.validate(certPath, pkixParams) + + true + } match { + case Success(_) => + println("Certificate is valid and trusted.") + true + case Failure(e: CertPathValidatorException) => + println(s"Certificate validation failed: ${e.getMessage}") + false + case Failure(e) => + println(s"Error: ${e.getMessage}") + false + } + } + + private def parsePemToX509Certificate(pem: String): X509Certificate = { + val cleanedPem = pem + .replaceAll("-----BEGIN CERTIFICATE-----", "") + .replaceAll("-----END CERTIFICATE-----", "") + .replaceAll("\\s", "") + + val decoded = Base64.getDecoder.decode(cleanedPem) + val certFactory = CertificateFactory.getInstance("X.509") + certFactory.generateCertificate(new ByteArrayInputStream(decoded)).asInstanceOf[X509Certificate] + } + + def main(args: Array[String]): Unit = { + val pemCertificate = + """-----BEGIN CERTIFICATE----- + MIIDFzCCAf+gAwIBAgIUPvfFnlyEm/bRwvPzhpfSxuI6XjkwDQYJKoZIhvcNAQELBQAwGzEZMBcGA1UEAwwQVGVzdCBDZXJ0aWZpY2F0ZTAeFw0yNTAyMTQwODM3NDhaFw0yNjAyMTQwODM3NDhaMBsxGTAXBgNVBAMMEFRlc3QgQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCk9Mj4YgJywaCduTLjAEd3o1OqzFaj2MuI/bcdKIwPlld0n8WHp+CMkbpCD8TSAlDrjLjxcL6Homw8SM3VYUJVP/5phRNgNx7E+KzquskPUsWvTUnylLF52jLjbKVXqs6DuukGAaJNudcuJCPuGd5xDTiymRdqFL1LFxSlaqt/qRS8DV9d3/Z0JwXuHebq17pjUGluq8nkJ0N1zF5hKLdQmo9PxVULY5Kubjf2cXoH09AgJUj3RSgeScRbFxgYOhU/5OaEfQuAST0Qa8lFI6SyWQp5G08wNZGITLh/66ZissNPYIUgqGccDFKWhUNDubFF+Qyl3Gy12g8Uou6FN1qrAgMBAAGjUzBRMB0GA1UdDgQWBBSN2MfohCTpCamhcyidj2w6z6tGXDAfBgNVHSMEGDAWgBSN2MfohCTpCamhcyidj2w6z6tGXDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBYXj3L5UN8PxJAMtLT9bU4FkxyXIQM+bzvAln1ZcAfHAGb2q49oJAXy4I22f9keuq3PV7OftsjZ888rjz9QU8vMSBejWT5GV4Ln5QmQXCHonmhq6DbP7BYb4DTOXfhvk+fdg0EDdqCpzDSCXdutOjjGU6P7L0769Zjpkrnk7uuqxZ8u/FslALeuq7cerBpsOUT5CJumpQxWcUCEbFxyZJTu5SXetgKJ9Dm62AfX5H69//z88W5TUzp66Mh4AWhEa/UByJGEw9SEsjFtYhkXluz5oFee5TGWTVZRlK08UrgH9JbiuyvPc9ZNL6Ek9fV54iajqsixZCfcICICtu8hZjZ + -----END CERTIFICATE-----""" + + val isValid = verifyCertificate(pemCertificate) + println(s"Certificate verification result: $isValid") + + + val defaultTrustStore = System.getProperty("javax.net.ssl.trustStore", "Default (cacerts)") + println(s"Default Trust Store: $defaultTrustStore") + + // Load and print all certificates in the default trust store + val trustStore = KeyStore.getInstance(KeyStore.getDefaultType) + val trustStorePath = Option(System.getProperty("javax.net.ssl.trustStore")) + .getOrElse("/usr/lib/jvm/java-17-openjdk-amd64/lib/security/cacerts") + + val trustStoreInputStream = new java.io.FileInputStream(trustStorePath) + trustStore.load(trustStoreInputStream, "changeit".toCharArray) // Default password: changeit + trustStoreInputStream.close() + + println(s"Trust Store contains ${trustStore.size()} certificates") + } +} From 6c3e68f6f71da71992bff92e0a1352e38af14fca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 18 Feb 2025 17:45:50 +0100 Subject: [PATCH 02/14] feature/Verify the certificate's trust chain up to a trusted certification authority 2 --- .../resources/props/sample.props.template | 6 ++ .../code/api/util/CertificateVerifier.scala | 71 +++++++++---------- 2 files changed, 41 insertions(+), 36 deletions(-) diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index e2a7ddba7..385f196ea 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -178,6 +178,12 @@ jwt.use.ssl=false # truststore.password.redis = truststore-password +## Trust stores is a list of trusted CA certificates +## Public certificate for the CA (used by clients and servers to validate signatures) +# truststore.path.tpp_signature = path/to/ca.p12 +# truststore.password.tpp_signature = truststore-password + + ## Enable writing API metrics (which APIs are called) to RDBMS write_metrics=true ## Enable writing connector metrics (which methods are called)to RDBMS diff --git a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala index 59f9bab14..4b3d2f3cf 100644 --- a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala +++ b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala @@ -1,6 +1,6 @@ package code.api.util -import java.io.ByteArrayInputStream +import java.io.{ByteArrayInputStream, FileInputStream} import java.security.KeyStore import java.security.cert._ import java.util.{Base64, Collections} @@ -10,25 +10,36 @@ import scala.util.{Failure, Success, Try} object CertificateVerifier { + // Load trust store from configured path and password + private def loadTrustStore(): Option[KeyStore] = { + val trustStorePath = APIUtil.getPropsValue("truststore.path.tpp_signature", "") + val trustStorePassword = APIUtil.getPropsValue("truststore.password.tpp_signature", "").toCharArray + + Try { + val trustStore = KeyStore.getInstance("PKCS12") // Using `.p12` format + val trustStoreInputStream = new FileInputStream(trustStorePath) + trustStore.load(trustStoreInputStream, trustStorePassword) + trustStoreInputStream.close() + trustStore + } match { + case Success(store) => + println(s"✅ Loaded trust store from: $trustStorePath") + Some(store) + case Failure(e) => + println(s"❌ Failed to load trust store: ${e.getMessage}") + None + } + } + def verifyCertificate(pemCertificate: String): Boolean = { Try { - // Convert PEM string to X.509 Certificate val certificate = parsePemToX509Certificate(pemCertificate) - // Load the default trust store (can be replaced with a custom one) - val trustStore = KeyStore.getInstance(KeyStore.getDefaultType) - - - val trustStorePath = Option(System.getProperty("javax.net.ssl.trustStore")) - .getOrElse("/usr/lib/jvm/java-17-openjdk-amd64/lib/security/cacerts") - - val trustStoreInputStream = new java.io.FileInputStream(trustStorePath) - trustStore.load(trustStoreInputStream, "changeit".toCharArray) // Default password: changeit - trustStoreInputStream.close() - + // Load trust store + val trustStore = loadTrustStore() + .getOrElse(throw new Exception("Trust store could not be loaded.")) val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm) - // trustStore.load(null, null) // Load default trust store trustManagerFactory.init(trustStore) // Get trusted CAs from the trust store @@ -50,23 +61,21 @@ object CertificateVerifier { val validator = CertPathValidator.getInstance("PKIX") validator.validate(certPath, pkixParams) + println("✅ Certificate is valid and trusted.") true } match { - case Success(_) => - println("Certificate is valid and trusted.") - true + case Success(_) => true case Failure(e: CertPathValidatorException) => - println(s"Certificate validation failed: ${e.getMessage}") + println(s"❌ Certificate validation failed: ${e.getMessage}") false case Failure(e) => - println(s"Error: ${e.getMessage}") + println(s"❌ Error: ${e.getMessage}") false } } private def parsePemToX509Certificate(pem: String): X509Certificate = { - val cleanedPem = pem - .replaceAll("-----BEGIN CERTIFICATE-----", "") + val cleanedPem = pem.replaceAll("-----BEGIN CERTIFICATE-----", "") .replaceAll("-----END CERTIFICATE-----", "") .replaceAll("\\s", "") @@ -82,21 +91,11 @@ object CertificateVerifier { -----END CERTIFICATE-----""" val isValid = verifyCertificate(pemCertificate) - println(s"Certificate verification result: $isValid") + println(s"✅ Certificate verification result: $isValid") - - val defaultTrustStore = System.getProperty("javax.net.ssl.trustStore", "Default (cacerts)") - println(s"Default Trust Store: $defaultTrustStore") - - // Load and print all certificates in the default trust store - val trustStore = KeyStore.getInstance(KeyStore.getDefaultType) - val trustStorePath = Option(System.getProperty("javax.net.ssl.trustStore")) - .getOrElse("/usr/lib/jvm/java-17-openjdk-amd64/lib/security/cacerts") - - val trustStoreInputStream = new java.io.FileInputStream(trustStorePath) - trustStore.load(trustStoreInputStream, "changeit".toCharArray) // Default password: changeit - trustStoreInputStream.close() - - println(s"Trust Store contains ${trustStore.size()} certificates") + // Display loaded trust store info + loadTrustStore().foreach { trustStore => + println(s"🔹 Trust Store contains ${trustStore.size()} certificates.") + } } } From 49f7efe0588a5b13bb881448667af1dbc25164cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 18 Feb 2025 17:46:31 +0100 Subject: [PATCH 03/14] bugfix/Load the CA certificate of Redis --- obp-api/src/main/scala/code/api/cache/Redis.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obp-api/src/main/scala/code/api/cache/Redis.scala b/obp-api/src/main/scala/code/api/cache/Redis.scala index 4c5412125..ed5d6856c 100644 --- a/obp-api/src/main/scala/code/api/cache/Redis.scala +++ b/obp-api/src/main/scala/code/api/cache/Redis.scala @@ -61,7 +61,7 @@ object Redis extends MdcLoggable { // Load the CA certificate val trustStore = KeyStore.getInstance(KeyStore.getDefaultType) - val trustStorePassword = APIUtil.getPropsValue("keystore.password.redis") + val trustStorePassword = APIUtil.getPropsValue("truststore.password.redis") .getOrElse(APIUtil.initPasswd).toCharArray val truststorePath = APIUtil.getPropsValue("truststore.path.redis").getOrElse("") val trustStoreStream = new FileInputStream(truststorePath) From b33d9a33669ddb5f1a1fd6770dbda69b80563da4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 19 Feb 2025 12:05:15 +0100 Subject: [PATCH 04/14] feature/Verify the certificate's trust chain up to a trusted certification authority 3 --- .../code/api/util/CertificateVerifier.scala | 92 +++++++++++++++---- 1 file changed, 73 insertions(+), 19 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala index 4b3d2f3cf..4a84db170 100644 --- a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala +++ b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala @@ -1,36 +1,66 @@ package code.api.util +import code.util.Helper.MdcLoggable + import java.io.{ByteArrayInputStream, FileInputStream} import java.security.KeyStore import java.security.cert._ import java.util.{Base64, Collections} import javax.net.ssl.TrustManagerFactory +import scala.io.Source import scala.jdk.CollectionConverters._ import scala.util.{Failure, Success, Try} -object CertificateVerifier { +object CertificateVerifier extends MdcLoggable { - // Load trust store from configured path and password + /** + * Loads a trust store (`.p12` file) from a configured path. + * + * This function: + * - Reads the trust store password from the application properties (`truststore.path.tpp_signature`). + * - Uses Java's `KeyStore` class to load the certificates. + * - If successful, logs `✅ Loaded trust store from: path`. + * - If it fails, logs `❌ Failed to load trust store: error message`. + * + * @return An `Option[KeyStore]` containing the loaded trust store, or `None` if loading fails. + */ private def loadTrustStore(): Option[KeyStore] = { - val trustStorePath = APIUtil.getPropsValue("truststore.path.tpp_signature", "") + val trustStorePath = APIUtil.getPropsValue("truststore.path.tpp_signature") + .or(APIUtil.getPropsValue("truststore.path")).getOrElse("") val trustStorePassword = APIUtil.getPropsValue("truststore.password.tpp_signature", "").toCharArray Try { - val trustStore = KeyStore.getInstance("PKCS12") // Using `.p12` format + val trustStore = KeyStore.getInstance("PKCS12") val trustStoreInputStream = new FileInputStream(trustStorePath) - trustStore.load(trustStoreInputStream, trustStorePassword) - trustStoreInputStream.close() + try { + trustStore.load(trustStoreInputStream, trustStorePassword) + } finally { + trustStoreInputStream.close() + } trustStore } match { case Success(store) => - println(s"✅ Loaded trust store from: $trustStorePath") + logger.info(s"✅ Loaded trust store from: $trustStorePath") Some(store) case Failure(e) => - println(s"❌ Failed to load trust store: ${e.getMessage}") + logger.info(s"❌ Failed to load trust store: ${e.getMessage}") None } } + /** + * Verifies an X.509 certificate against the loaded trust store. + * + * This function: + * - Parses the PEM certificate into an `X509Certificate` using `parsePemToX509Certificate`. + * - Loads the trust store using `loadTrustStore()`. + * - Extracts trusted root CAs from the trust store. + * - Creates PKIX validation parameters and disables revocation checking. + * - Validates the certificate using Java's `CertPathValidator`. + * + * @param pemCertificate The X.509 certificate in PEM format. + * @return `true` if the certificate is valid and trusted, otherwise `false`. + */ def verifyCertificate(pemCertificate: String): Boolean = { Try { val certificate = parsePemToX509Certificate(pemCertificate) @@ -61,19 +91,30 @@ object CertificateVerifier { val validator = CertPathValidator.getInstance("PKIX") validator.validate(certPath, pkixParams) - println("✅ Certificate is valid and trusted.") + logger.info("✅ Certificate is valid and trusted.") true } match { case Success(_) => true case Failure(e: CertPathValidatorException) => - println(s"❌ Certificate validation failed: ${e.getMessage}") + logger.info(s"❌ Certificate validation failed: ${e.getMessage}") false case Failure(e) => - println(s"❌ Error: ${e.getMessage}") + logger.info(s"❌ Error: ${e.getMessage}") false } } + /** + * Converts a PEM certificate (Base64-encoded) into an `X509Certificate` object. + * + * This function: + * - Removes the PEM header and footer (`-----BEGIN CERTIFICATE-----` and `-----END CERTIFICATE-----`). + * - Decodes the Base64-encoded certificate data. + * - Generates and returns an `X509Certificate` object. + * + * @param pem The X.509 certificate in PEM format. + * @return The parsed `X509Certificate` object. + */ private def parsePemToX509Certificate(pem: String): X509Certificate = { val cleanedPem = pem.replaceAll("-----BEGIN CERTIFICATE-----", "") .replaceAll("-----END CERTIFICATE-----", "") @@ -84,18 +125,31 @@ object CertificateVerifier { certFactory.generateCertificate(new ByteArrayInputStream(decoded)).asInstanceOf[X509Certificate] } + def loadPemCertificateFromFile(filePath: String): Option[String] = { + Try { + val source = Source.fromFile(filePath) + try source.getLines().mkString("\n") // Read entire file into a single string + finally source.close() + } match { + case Success(pem) => Some(pem) + case Failure(exception) => + println(s"❌ Failed to load PEM certificate from file: ${exception.getMessage}") + None + } + } + def main(args: Array[String]): Unit = { - val pemCertificate = - """-----BEGIN CERTIFICATE----- - MIIDFzCCAf+gAwIBAgIUPvfFnlyEm/bRwvPzhpfSxuI6XjkwDQYJKoZIhvcNAQELBQAwGzEZMBcGA1UEAwwQVGVzdCBDZXJ0aWZpY2F0ZTAeFw0yNTAyMTQwODM3NDhaFw0yNjAyMTQwODM3NDhaMBsxGTAXBgNVBAMMEFRlc3QgQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCk9Mj4YgJywaCduTLjAEd3o1OqzFaj2MuI/bcdKIwPlld0n8WHp+CMkbpCD8TSAlDrjLjxcL6Homw8SM3VYUJVP/5phRNgNx7E+KzquskPUsWvTUnylLF52jLjbKVXqs6DuukGAaJNudcuJCPuGd5xDTiymRdqFL1LFxSlaqt/qRS8DV9d3/Z0JwXuHebq17pjUGluq8nkJ0N1zF5hKLdQmo9PxVULY5Kubjf2cXoH09AgJUj3RSgeScRbFxgYOhU/5OaEfQuAST0Qa8lFI6SyWQp5G08wNZGITLh/66ZissNPYIUgqGccDFKWhUNDubFF+Qyl3Gy12g8Uou6FN1qrAgMBAAGjUzBRMB0GA1UdDgQWBBSN2MfohCTpCamhcyidj2w6z6tGXDAfBgNVHSMEGDAWgBSN2MfohCTpCamhcyidj2w6z6tGXDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBYXj3L5UN8PxJAMtLT9bU4FkxyXIQM+bzvAln1ZcAfHAGb2q49oJAXy4I22f9keuq3PV7OftsjZ888rjz9QU8vMSBejWT5GV4Ln5QmQXCHonmhq6DbP7BYb4DTOXfhvk+fdg0EDdqCpzDSCXdutOjjGU6P7L0769Zjpkrnk7uuqxZ8u/FslALeuq7cerBpsOUT5CJumpQxWcUCEbFxyZJTu5SXetgKJ9Dm62AfX5H69//z88W5TUzp66Mh4AWhEa/UByJGEw9SEsjFtYhkXluz5oFee5TGWTVZRlK08UrgH9JbiuyvPc9ZNL6Ek9fV54iajqsixZCfcICICtu8hZjZ - -----END CERTIFICATE-----""" + // val certificatePath = "/path/to/certificate.pem" + val certificatePath = "/home/marko/Downloads/BerlinGroupSigning/certificate.pem" + val pemCertificate = loadPemCertificateFromFile(certificatePath) - val isValid = verifyCertificate(pemCertificate) - println(s"✅ Certificate verification result: $isValid") + pemCertificate.foreach { pem => + val isValid = verifyCertificate(pem) + logger.info(s"✅ Certificate verification result: $isValid") + } - // Display loaded trust store info loadTrustStore().foreach { trustStore => - println(s"🔹 Trust Store contains ${trustStore.size()} certificates.") + logger.info(s"🔹 Trust Store contains ${trustStore.size()} certificates.") } } } From fc91b2ec255d809c3afda78bd2fac5fc80f6d40b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Thu, 20 Feb 2025 10:47:31 +0100 Subject: [PATCH 05/14] feature/Verify the certificate's trust chain up to a trusted certification authority 4 --- .../src/main/scala/code/api/util/BerlinGroupSigning.scala | 7 ++++++- .../src/main/scala/code/api/util/CertificateVerifier.scala | 4 ++-- obp-api/src/main/scala/code/api/util/ErrorMessages.scala | 1 + 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala b/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala index 3cb253db0..c63fc89cc 100644 --- a/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala +++ b/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala @@ -130,7 +130,12 @@ object BerlinGroupSigning { val signatureHeaderValue = getHeaderValue(RequestHeader.Signature, requestHeaders) val signature = parseSignatureHeader(signatureHeaderValue).getOrElse("signature", "NONE") val isVerified = verifySignature(signingString, signature, certificatePem) - if (isVerified) forwardResult else (Failure(ErrorMessages.X509PublicKeyCannotVerify), forwardResult._2) + val isValidated = CertificateVerifier.validateCertificate(certificatePem) + (isVerified, isValidated) match { + case (true, true) => forwardResult + case (true, false) => (Failure(ErrorMessages.X509PublicKeyCannotBeValidated), forwardResult._2) + case (false, _) => (Failure(ErrorMessages.X509PublicKeyCannotVerify), forwardResult._2) + } case Failure(msg, t, c) => (Failure(msg, t, c), forwardResult._2) // PEM certificate is not valid case _ => (Failure(ErrorMessages.X509GeneralError), forwardResult._2) // PEM certificate cannot be validated } diff --git a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala index 4a84db170..848c4efc5 100644 --- a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala +++ b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala @@ -61,7 +61,7 @@ object CertificateVerifier extends MdcLoggable { * @param pemCertificate The X.509 certificate in PEM format. * @return `true` if the certificate is valid and trusted, otherwise `false`. */ - def verifyCertificate(pemCertificate: String): Boolean = { + def validateCertificate(pemCertificate: String): Boolean = { Try { val certificate = parsePemToX509Certificate(pemCertificate) @@ -144,7 +144,7 @@ object CertificateVerifier extends MdcLoggable { val pemCertificate = loadPemCertificateFromFile(certificatePath) pemCertificate.foreach { pem => - val isValid = verifyCertificate(pem) + val isValid = validateCertificate(pem) logger.info(s"✅ Certificate verification result: $isValid") } diff --git a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala index 37061e2c2..e26c48520 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -279,6 +279,7 @@ object ErrorMessages { val X509ThereAreNoPsd2Roles = "OBP-20308: PEM Encoded Certificate does not contain PSD2 roles." val X509CannotGetPublicKey = "OBP-20309: Public key cannot be found in the PEM Encoded Certificate." val X509PublicKeyCannotVerify = "OBP-20310: Certificate's public key cannot be used to verify signed request." + val X509PublicKeyCannotBeValidated = "OBP-20312: Certificate's public key cannot be validated." val X509RequestIsNotSigned = "OBP-20311: The Request is not signed." // OpenID Connect From 4ddc3e627b6231fb9fc4b269bcaac4c1c4e454e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Fri, 21 Feb 2025 17:09:14 +0100 Subject: [PATCH 06/14] feature/Implement BG consent flow when user chooses accounts to link at confirmation step --- .../code/snippet/BerlinGroupConsent.scala | 225 ++++++++++++++++-- .../webapp/confirm-bg-consent-request.html | 2 +- 2 files changed, 203 insertions(+), 24 deletions(-) diff --git a/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala b/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala index 9b8956ef9..8d396dae9 100644 --- a/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala +++ b/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala @@ -26,30 +26,86 @@ TESOBE (http://www.tesobe.com/) */ package code.snippet +import code.accountholders.AccountHolders import code.api.RequestHeader -import code.api.berlin.group.v1_3.JSONFactory_BERLIN_GROUP_1_3.{GetConsentResponseJson, createGetConsentResponseJson} -import code.api.util.{ConsentJWT, CustomJsonFormats, JwtUtil} +import code.api.berlin.group.v1_3.JSONFactory_BERLIN_GROUP_1_3.{ConsentAccessAccountsJson, ConsentAccessJson, GetConsentResponseJson, createGetConsentResponseJson} +import code.api.util.ErrorMessages.ConsentNotFound +import code.api.util._ import code.api.v3_1_0.APIMethods310 import code.api.v5_0_0.APIMethods500 import code.api.v5_1_0.APIMethods510 import code.consent.{ConsentStatus, Consents, MappedConsent} import code.consumer.Consumers -import code.model.dataAccess.AuthUser +import code.model.dataAccess.{AuthUser, BankAccountRouting} import code.util.Helper.{MdcLoggable, ObpS} +import com.openbankproject.commons.ExecutionContext.Implicits.global +import com.openbankproject.commons.model.BankIdAccountId import net.liftweb.common.{Box, Failure, Full} +import net.liftweb.http.js.JsCmds import net.liftweb.http.rest.RestHelper -import net.liftweb.http.{RequestVar, S, SHtml, SessionVar} +import net.liftweb.http.{S, SHtml, SessionVar} import net.liftweb.json.{Formats, parse} +import net.liftweb.mapper.By import net.liftweb.util.CssSel import net.liftweb.util.Helpers._ import scala.collection.immutable +import scala.concurrent.Future +import scala.xml.NodeSeq class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 with APIMethods500 with APIMethods310 { protected implicit override def formats: Formats = CustomJsonFormats.formats private object otpValue extends SessionVar("123") private object redirectUriValue extends SessionVar("") + private object updateConsentPayloadValue extends SessionVar(false) + private object userIsOwnerOfAccountsValue extends SessionVar(true) + + // Separate session variables for accounts, balances, and transactions + private object selectedAccountsIbansValue extends SessionVar[Set[String]](Set()) { + override def set(value: Set[String]): Set[String] = { + logger.debug(s"selectedAccountsIbansValue changed to: ${value.mkString(", ")}") + super.set(value) + } + } + + private object selectedBalancesIbansValue extends SessionVar[Set[String]](Set()) + + private object selectedTransactionsIbansValue extends SessionVar[Set[String]](Set()) + + // Function to transform a list of IBANs into ConsentAccessJson + def createConsentAccessJson(accounts: List[String], balances: List[String], transactions: List[String]): ConsentAccessJson = { + val accountsList = accounts.map(iban => ConsentAccessAccountsJson(iban = Some(iban), None, None, None, None, None)) + val balancesList = balances.map(iban => ConsentAccessAccountsJson(iban = Some(iban), None, None, None, None, None)) + val transactionsList = transactions.map(iban => ConsentAccessAccountsJson(iban = Some(iban), None, None, None, None, None)) + + ConsentAccessJson( + accounts = Some(accountsList), // Populate accounts + balances = Some(balancesList), // Populate balances + transactions = Some(transactionsList) // Populate transactions + ) + } + + private def updateConsent(consentId: String, ibansAccount: List[String], ibansBalance: List[String], ibansTransaction: List[String]): Future[MappedConsent] = { + for { + consent: MappedConsent <- Future(Consents.consentProvider.vend.getConsentByConsentId(consentId)) map { + APIUtil.unboxFullOrFail(_, None, s"$ConsentNotFound ($consentId)", 404) + } + consentJWT <- Consent.updateAccountAccessOfBerlinGroupConsentJWT( + createConsentAccessJson(ibansAccount, ibansBalance, ibansTransaction), + consent, + None + ) map { + i => APIUtil.connectorEmptyResponse(i, None) + } + updatedConsent <- Future(Consents.consentProvider.vend.setJsonWebToken(consent.consentId, consentJWT)) map { + i => APIUtil.connectorEmptyResponse(i, None) + } + } yield { + updatedConsent + } + } + def confirmBerlinGroupConsentRequest: CssSel = { callGetConsentByConsentId() match { @@ -65,25 +121,137 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 val consumerRedirectUri: Option[String] = consumer.map(_.redirectURL.get).toOption val uri: String = tppRedirectUri.headOption.orElse(consumerRedirectUri).getOrElse("https://not.defined.com") redirectUriValue.set(uri) + + //Get All OBP accounts from `Account Holder` table, source == null --> mean accounts are created by OBP endpoints, not from User Auth Context, + // Step 1: Get all accounts held by the current user + val userAccounts: Set[BankIdAccountId] = + AccountHolders.accountHolders.vend.getAccountsHeldByUser(AuthUser.currentUser.flatMap(_.user.foreign).openOrThrowException(ErrorMessages.UserNotLoggedIn), Some(null)).toSet + val userIbans: Set[String] = userAccounts.flatMap { acc => + BankAccountRouting.find( + By(BankAccountRouting.BankId, acc.bankId.value), + By(BankAccountRouting.AccountId, acc.accountId.value), + By(BankAccountRouting.AccountRoutingScheme, "IBAN") + ).map(_.AccountRoutingAddress.get) + } + + + val canReadAccountsIbans: List[String] = json.access.accounts match { + case Some(accounts) if accounts.isEmpty => + updateConsentPayloadValue.set(true) + userIbans.toList + case Some(accounts) if accounts.flatMap(_.iban).toSet.subsetOf(userIbans) => + accounts.flatMap(_.iban) + case Some(accounts) => + userIsOwnerOfAccountsValue.set(false) + accounts.flatMap(_.iban) + case None => List() + } + val canReadBalancesIbans: List[String] = json.access.balances match { + case Some(balances) if balances.isEmpty => + updateConsentPayloadValue.set(true) + userIbans.toList + case Some(balances) if balances.flatMap(_.iban).toSet.subsetOf(userIbans) => + balances.flatMap(_.iban) + case Some(balances) => + userIsOwnerOfAccountsValue.set(false) + balances.flatMap(_.iban) + case None => List() + } + val canReadTransactionsIbans: List[String] = json.access.transactions match { + case Some(transactions) if transactions.isEmpty => + updateConsentPayloadValue.set(true) + userIbans.toList + case Some(transactions) if transactions.flatMap(_.iban).toSet.subsetOf(userIbans) => + transactions.flatMap(_.iban) + case Some(transactions) => + userIsOwnerOfAccountsValue.set(false) + transactions.flatMap(_.iban) + case None => List() + } + + /// Function to generate toggle switches for IBAN lists + def generateCheckboxes(scope: String, ibans: List[String], selectedList: Set[String], sessionVar: SessionVar[Set[String]]): immutable.Seq[NodeSeq] = { + ibans.map { iban => + if (updateConsentPayloadValue.is) { + // Show toggle switch when updateConsentPayloadValue is true +
+ + + {iban} + +
+ } else { + // Show only the IBAN text when updateConsentPayloadValue is false + + {iban} + + } + } + } + + + // Form text and user details + val currentUser = AuthUser.currentUser + val firstName = currentUser.map(_.firstName.get).getOrElse("") + val lastName = currentUser.map(_.lastName.get).getOrElse("") + val consumerName = consumer.map(_.name.get).getOrElse("") val formText = - s"""I, ${AuthUser.currentUser.map(_.firstName.get).getOrElse("")} ${AuthUser.currentUser.map(_.lastName.get).getOrElse("")}, consent to the service provider ${consumer.map(_.name.get).getOrElse("")} making actions on my behalf. - | - |This consent must respects the following actions: - | - | 1) Can read accounts: ${json.access.accounts.getOrElse(Nil).flatMap(_.iban).mkString(", ")} - | 2) Can read balances: ${json.access.balances.getOrElse(Nil).flatMap(_.iban).mkString(", ")} - | 3) Can read transactions: ${json.access.transactions.getOrElse(Nil).flatMap(_.iban).mkString(", ")} - | - |This consent will end on date ${json.validUntil}. - | - |I understand that I can revoke this consent at any time. - |""".stripMargin + s"""I, $firstName $lastName, consent to the service provider $consumerName making the following actions on my behalf: + |""".stripMargin + // Converting formText into a NodeSeq for raw HTML + val formTextHtml: NodeSeq = scala.xml.XML.loadString("
" + formText + "
") - "#confirm-bg-consent-request-form-title *" #> s"Please confirm or deny the following consent request:" & - "#confirm-bg-consent-request-form-text *" #> s"""$formText""" & + // Form rendering + "#confirm-bg-consent-request-form-title *" #> "Please confirm or deny the following consent request:" & + "#confirm-bg-consent-request-form-text *" #> ( +
+

+ {formTextHtml} +

+ +

1) Read account (basic) details of:

+
+ {generateCheckboxes("canReadAccountsIbans", canReadAccountsIbans, selectedAccountsIbansValue.is, selectedAccountsIbansValue)} +
+
+ +

2) Read account balances of:

+
+ {generateCheckboxes("canReadBalancesIbans", canReadBalancesIbans, selectedBalancesIbansValue.is, selectedBalancesIbansValue)} +
+
+ +

3) Read transactions of:

+
+ {generateCheckboxes("canReadTransactionsIbans", canReadTransactionsIbans, selectedTransactionsIbansValue.is, selectedTransactionsIbansValue)} +
+
+ +

This consent will end on date + {json.validUntil} + .

+

I understand that I can revoke this consent at any time.

+
+ ) & { + if (userIsOwnerOfAccountsValue) { "#confirm-bg-consent-request-confirm-submit-button" #> SHtml.onSubmitUnit(confirmConsentRequestProcess) & - "#confirm-bg-consent-request-deny-submit-button" #> SHtml.onSubmitUnit(denyConsentRequestProcess) + "#confirm-bg-consent-request-deny-submit-button" #> SHtml.onSubmitUnit(denyConsentRequestProcess) + } else { + S.error(s"User $firstName $lastName is not owner of listed accounts") + "#confirm-bg-consent-request-confirm-submit-button" #> "" & + "#confirm-bg-consent-request-deny-submit-button" #> "" + }} + case everythingElse => S.error(everythingElse.toString) "#confirm-bg-consent-request-form-title *" #> s"Please confirm or deny the following consent request:" & @@ -104,10 +272,21 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 } private def confirmConsentRequestProcess() = { - val consentId = ObpS.param("CONSENT_ID") openOr ("") - S.redirectTo( - s"/confirm-bg-consent-request-sca?CONSENT_ID=${consentId}" - ) + if(selectedAccountsIbansValue.is.isEmpty && + selectedBalancesIbansValue.is.isEmpty && + selectedTransactionsIbansValue.is.isEmpty) + { + S.error(s"Please select at least 1 account") + } else { + val consentId = ObpS.param("CONSENT_ID") openOr ("") + if (updateConsentPayloadValue.is) { + updateConsent(consentId, selectedAccountsIbansValue.is.toList, selectedBalancesIbansValue.is.toList, selectedTransactionsIbansValue.is.toList) + } + S.redirectTo( + s"/confirm-bg-consent-request-sca?CONSENT_ID=${consentId}" + ) + } + } private def denyConsentRequestProcess() = { val consentId = ObpS.param("CONSENT_ID") openOr ("") diff --git a/obp-api/src/main/webapp/confirm-bg-consent-request.html b/obp-api/src/main/webapp/confirm-bg-consent-request.html index d3e2ffcff..d07e0318a 100644 --- a/obp-api/src/main/webapp/confirm-bg-consent-request.html +++ b/obp-api/src/main/webapp/confirm-bg-consent-request.html @@ -32,7 +32,7 @@ Berlin 13359, Germany
From 9a2f4b1fb7d7666c9e9eecd98e001c50a38bed12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Mon, 24 Feb 2025 13:51:49 +0100 Subject: [PATCH 07/14] docfix/Implement BG consent flow when user chooses accounts to link at confirmation step --- .../code/snippet/BerlinGroupConsent.scala | 147 ++++++++++++------ 1 file changed, 100 insertions(+), 47 deletions(-) diff --git a/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala b/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala index 8d396dae9..f082b4455 100644 --- a/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala +++ b/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala @@ -1,29 +1,28 @@ /** -Open Bank Project - API -Copyright (C) 2011-2019, TESOBE GmbH. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . - -Email: contact@tesobe.com -TESOBE GmbH. -Osloer Strasse 16/17 -Berlin 13359, Germany - -This product includes software developed at -TESOBE (http://www.tesobe.com/) - - */ + * Open Bank Project - API + * Copyright (C) 2011-2019, TESOBE GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * Email: contact@tesobe.com + * TESOBE GmbH. + * Osloer Strasse 16/17 + * Berlin 13359, Germany + * + * This product includes software developed at + * TESOBE (http://www.tesobe.com/) + */ package code.snippet import code.accountholders.AccountHolders @@ -53,15 +52,22 @@ import scala.collection.immutable import scala.concurrent.Future import scala.xml.NodeSeq +/** + * This class handles Berlin Group consent requests. + * It provides functionality to confirm or deny consent requests, + * and manages the consent process for accessing account data. + */ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 with APIMethods500 with APIMethods310 { + // Custom JSON formats for serialization/deserialization protected implicit override def formats: Formats = CustomJsonFormats.formats - private object otpValue extends SessionVar("123") - private object redirectUriValue extends SessionVar("") - private object updateConsentPayloadValue extends SessionVar(false) - private object userIsOwnerOfAccountsValue extends SessionVar(true) + // Session variables to store OTP, redirect URI, and other consent-related data + private object otpValue extends SessionVar("123") // Stores the OTP value for SCA (Strong Customer Authentication) + private object redirectUriValue extends SessionVar("") // Stores the redirect URI for post-consent actions + private object updateConsentPayloadValue extends SessionVar(false) // Flag to indicate if consent payload needs updating + private object userIsOwnerOfAccountsValue extends SessionVar(true) // Flag to check if the user owns the accounts - // Separate session variables for accounts, balances, and transactions + // Session variables to store selected IBANs for accounts, balances, and transactions private object selectedAccountsIbansValue extends SessionVar[Set[String]](Set()) { override def set(value: Set[String]): Set[String] = { logger.debug(s"selectedAccountsIbansValue changed to: ${value.mkString(", ")}") @@ -69,11 +75,17 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 } } - private object selectedBalancesIbansValue extends SessionVar[Set[String]](Set()) + private object selectedBalancesIbansValue extends SessionVar[Set[String]](Set()) // Stores selected IBANs for balances + private object selectedTransactionsIbansValue extends SessionVar[Set[String]](Set()) // Stores selected IBANs for transactions - private object selectedTransactionsIbansValue extends SessionVar[Set[String]](Set()) - - // Function to transform a list of IBANs into ConsentAccessJson + /** + * Creates a ConsentAccessJson object from lists of IBANs for accounts, balances, and transactions. + * + * @param accounts List of IBANs for accounts. + * @param balances List of IBANs for balances. + * @param transactions List of IBANs for transactions. + * @return ConsentAccessJson object. + */ def createConsentAccessJson(accounts: List[String], balances: List[String], transactions: List[String]): ConsentAccessJson = { val accountsList = accounts.map(iban => ConsentAccessAccountsJson(iban = Some(iban), None, None, None, None, None)) val balancesList = balances.map(iban => ConsentAccessAccountsJson(iban = Some(iban), None, None, None, None, None)) @@ -86,11 +98,22 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 ) } + /** + * Updates the consent with new IBANs for accounts, balances, and transactions. + * + * @param consentId The ID of the consent to update. + * @param ibansAccount List of IBANs for accounts. + * @param ibansBalance List of IBANs for balances. + * @param ibansTransaction List of IBANs for transactions. + * @return Future[MappedConsent] representing the updated consent. + */ private def updateConsent(consentId: String, ibansAccount: List[String], ibansBalance: List[String], ibansTransaction: List[String]): Future[MappedConsent] = { for { + // Fetch the consent by ID consent: MappedConsent <- Future(Consents.consentProvider.vend.getConsentByConsentId(consentId)) map { APIUtil.unboxFullOrFail(_, None, s"$ConsentNotFound ($consentId)", 404) } + // Update the consent JWT with new access details consentJWT <- Consent.updateAccountAccessOfBerlinGroupConsentJWT( createConsentAccessJson(ibansAccount, ibansBalance, ibansTransaction), consent, @@ -98,6 +121,7 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 ) map { i => APIUtil.connectorEmptyResponse(i, None) } + // Save the updated consent updatedConsent <- Future(Consents.consentProvider.vend.setJsonWebToken(consent.consentId, consentJWT)) map { i => APIUtil.connectorEmptyResponse(i, None) } @@ -106,26 +130,30 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 } } - + /** + * Renders the Berlin Group consent confirmation form. + * + * @return CssSel for rendering the form. + */ def confirmBerlinGroupConsentRequest: CssSel = { callGetConsentByConsentId() match { case Full(consent) => + // Set OTP and redirect URI from the consent otpValue.set(consent.challenge) val json: GetConsentResponseJson = createGetConsentResponseJson(consent) val consumer = Consumers.consumers.vend.getConsumerByConsumerId(consent.consumerId) val consentJwt: Box[ConsentJWT] = JwtUtil.getSignedPayloadAsJson(consent.jsonWebToken).map(parse(_) .extract[ConsentJWT]) - val tppRedirectUri: immutable.Seq[String] = consentJwt.map{ h => + val tppRedirectUri: immutable.Seq[String] = consentJwt.map { h => h.request_headers.filter(h => h.name == RequestHeader.`TPP-Redirect-URL`) }.getOrElse(Nil).map((_.values.mkString(""))) val consumerRedirectUri: Option[String] = consumer.map(_.redirectURL.get).toOption val uri: String = tppRedirectUri.headOption.orElse(consumerRedirectUri).getOrElse("https://not.defined.com") redirectUriValue.set(uri) - //Get All OBP accounts from `Account Holder` table, source == null --> mean accounts are created by OBP endpoints, not from User Auth Context, - // Step 1: Get all accounts held by the current user + // Get all accounts held by the current user val userAccounts: Set[BankIdAccountId] = - AccountHolders.accountHolders.vend.getAccountsHeldByUser(AuthUser.currentUser.flatMap(_.user.foreign).openOrThrowException(ErrorMessages.UserNotLoggedIn), Some(null)).toSet + AccountHolders.accountHolders.vend.getAccountsHeldByUser(AuthUser.currentUser.flatMap(_.user.foreign).openOrThrowException(ErrorMessages.UserNotLoggedIn), Some(null)).toSet val userIbans: Set[String] = userAccounts.flatMap { acc => BankAccountRouting.find( By(BankAccountRouting.BankId, acc.bankId.value), @@ -134,7 +162,7 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 ).map(_.AccountRoutingAddress.get) } - + // Determine which IBANs the user can access for accounts, balances, and transactions val canReadAccountsIbans: List[String] = json.access.accounts match { case Some(accounts) if accounts.isEmpty => updateConsentPayloadValue.set(true) @@ -169,7 +197,15 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 case None => List() } - /// Function to generate toggle switches for IBAN lists + /** + * Generates toggle switches for IBAN lists. + * + * @param scope The scope of the IBANs (e.g., "canReadAccountsIbans"). + * @param ibans List of IBANs to display. + * @param selectedList Set of currently selected IBANs. + * @param sessionVar Session variable to update when toggling. + * @return Sequence of NodeSeq representing the toggle switches. + */ def generateCheckboxes(scope: String, ibans: List[String], selectedList: Set[String], sessionVar: SessionVar[Set[String]]): immutable.Seq[NodeSeq] = { ibans.map { iban => if (updateConsentPayloadValue.is) { @@ -198,7 +234,6 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 } } - // Form text and user details val currentUser = AuthUser.currentUser val firstName = currentUser.map(_.firstName.get).getOrElse("") @@ -259,6 +294,11 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 } } + /** + * Fetches a consent by its ID. + * + * @return Box[MappedConsent] containing the consent if found. + */ private def callGetConsentByConsentId(): Box[MappedConsent] = { val requestParam = List( ObpS.param("CONSENT_ID"), @@ -271,11 +311,13 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 } } + /** + * Handles the confirmation of a consent request. + */ private def confirmConsentRequestProcess() = { - if(selectedAccountsIbansValue.is.isEmpty && + if (selectedAccountsIbansValue.is.isEmpty && selectedBalancesIbansValue.is.isEmpty && - selectedTransactionsIbansValue.is.isEmpty) - { + selectedTransactionsIbansValue.is.isEmpty) { S.error(s"Please select at least 1 account") } else { val consentId = ObpS.param("CONSENT_ID") openOr ("") @@ -286,8 +328,11 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 s"/confirm-bg-consent-request-sca?CONSENT_ID=${consentId}" ) } - } + + /** + * Handles the denial of a consent request. + */ private def denyConsentRequestProcess() = { val consentId = ObpS.param("CONSENT_ID") openOr ("") Consents.consentProvider.vend.updateConsentStatus(consentId, ConsentStatus.rejected) @@ -295,6 +340,10 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 s"$redirectUriValue?CONSENT_ID=${consentId}" ) } + + /** + * Handles the confirmation of a consent request with SCA (Strong Customer Authentication). + */ private def confirmConsentRequestProcessSca() = { val consentId = ObpS.param("CONSENT_ID") openOr ("") Consents.consentProvider.vend.getConsentByConsentId(consentId) match { @@ -308,7 +357,11 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 } } - + /** + * Renders the SCA confirmation form for Berlin Group consent. + * + * @return CssSel for rendering the form. + */ def confirmBgConsentRequest: CssSel = { "#otp-value" #> SHtml.text(otpValue, otpValue(_)) & "type=submit" #> SHtml.onSubmitUnit(confirmConsentRequestProcessSca) From 1d80588a599ab9b1dbd61ce860dbc87c6b9578f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 25 Feb 2025 13:41:50 +0100 Subject: [PATCH 08/14] docfix/Implement BG consent flow when user chooses accounts to link at confirmation step 2 --- .../code/snippet/BerlinGroupConsent.scala | 107 ++++++++++-------- 1 file changed, 60 insertions(+), 47 deletions(-) diff --git a/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala b/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala index f082b4455..59bcbfb57 100644 --- a/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala +++ b/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala @@ -74,10 +74,9 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 super.set(value) } } - - private object selectedBalancesIbansValue extends SessionVar[Set[String]](Set()) // Stores selected IBANs for balances - private object selectedTransactionsIbansValue extends SessionVar[Set[String]](Set()) // Stores selected IBANs for transactions - + private object accessAccountsDefinedVar extends SessionVar(true) + private object accessBalancesDefinedVar extends SessionVar(true) + private object accessTransactionsDefinedVar extends SessionVar(true) /** * Creates a ConsentAccessJson object from lists of IBANs for accounts, balances, and transactions. * @@ -93,8 +92,8 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 ConsentAccessJson( accounts = Some(accountsList), // Populate accounts - balances = Some(balancesList), // Populate balances - transactions = Some(transactionsList) // Populate transactions + balances = if (balancesList.nonEmpty) Some(balancesList) else None, // Populate balances + transactions = if (transactionsList.nonEmpty) Some(transactionsList) else None // Populate transactions ) } @@ -102,12 +101,10 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 * Updates the consent with new IBANs for accounts, balances, and transactions. * * @param consentId The ID of the consent to update. - * @param ibansAccount List of IBANs for accounts. - * @param ibansBalance List of IBANs for balances. - * @param ibansTransaction List of IBANs for transactions. + * @param ibans List of IBANs for accounts. * @return Future[MappedConsent] representing the updated consent. */ - private def updateConsent(consentId: String, ibansAccount: List[String], ibansBalance: List[String], ibansTransaction: List[String]): Future[MappedConsent] = { + private def updateConsent(consentId: String, ibans: List[String], canReadBalances: Boolean, canReadTransactions: Boolean): Future[MappedConsent] = { for { // Fetch the consent by ID consent: MappedConsent <- Future(Consents.consentProvider.vend.getConsentByConsentId(consentId)) map { @@ -115,7 +112,11 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 } // Update the consent JWT with new access details consentJWT <- Consent.updateAccountAccessOfBerlinGroupConsentJWT( - createConsentAccessJson(ibansAccount, ibansBalance, ibansTransaction), + createConsentAccessJson( + ibans, + if(canReadBalances) ibans else List(), + if(canReadTransactions) ibans else List() + ), consent, None ) map { @@ -161,40 +162,57 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 By(BankAccountRouting.AccountRoutingScheme, "IBAN") ).map(_.AccountRoutingAddress.get) } + // Select all IBANs + selectedAccountsIbansValue.set(userIbans) // Determine which IBANs the user can access for accounts, balances, and transactions val canReadAccountsIbans: List[String] = json.access.accounts match { - case Some(accounts) if accounts.isEmpty => + case Some(accounts) if accounts.isEmpty => // Access is requested updateConsentPayloadValue.set(true) + accessAccountsDefinedVar.set(true) userIbans.toList - case Some(accounts) if accounts.flatMap(_.iban).toSet.subsetOf(userIbans) => + case Some(accounts) if accounts.flatMap(_.iban).toSet.subsetOf(userIbans) => // Access is requested for specific IBANs + accessAccountsDefinedVar.set(true) accounts.flatMap(_.iban) - case Some(accounts) => + case Some(accounts) => // Logged in user is not an owner of IBAN/IBANs userIsOwnerOfAccountsValue.set(false) + accessAccountsDefinedVar.set(true) accounts.flatMap(_.iban) - case None => List() + case None => // Access is not requested + accessAccountsDefinedVar.set(false) + List() } val canReadBalancesIbans: List[String] = json.access.balances match { - case Some(balances) if balances.isEmpty => + case Some(balances) if balances.isEmpty => // Access is requested updateConsentPayloadValue.set(true) + accessBalancesDefinedVar.set(true) userIbans.toList - case Some(balances) if balances.flatMap(_.iban).toSet.subsetOf(userIbans) => + case Some(balances) if balances.flatMap(_.iban).toSet.subsetOf(userIbans) => // Access is requested for specific IBANs + accessBalancesDefinedVar.set(true) balances.flatMap(_.iban) - case Some(balances) => + case Some(balances) => // Logged in user is not an owner of IBAN/IBANs userIsOwnerOfAccountsValue.set(false) + accessBalancesDefinedVar.set(true) balances.flatMap(_.iban) - case None => List() + case None => // Access is not requested + accessBalancesDefinedVar.set(false) + List() } val canReadTransactionsIbans: List[String] = json.access.transactions match { - case Some(transactions) if transactions.isEmpty => + case Some(transactions) if transactions.isEmpty => // Access is requested updateConsentPayloadValue.set(true) + accessTransactionsDefinedVar.set(true) userIbans.toList - case Some(transactions) if transactions.flatMap(_.iban).toSet.subsetOf(userIbans) => + case Some(transactions) if transactions.flatMap(_.iban).toSet.subsetOf(userIbans) => // Access is requested for specific IBANs + accessTransactionsDefinedVar.set(true) transactions.flatMap(_.iban) - case Some(transactions) => + case Some(transactions) => // Logged in user is not an owner of IBAN/IBANs userIsOwnerOfAccountsValue.set(false) + accessTransactionsDefinedVar.set(true) transactions.flatMap(_.iban) - case None => List() + case None => // Access is not requested + accessTransactionsDefinedVar.set(false) + List() } /** @@ -253,28 +271,20 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510

{formTextHtml}

- -

1) Read account (basic) details of:

-
- {generateCheckboxes("canReadAccountsIbans", canReadAccountsIbans, selectedAccountsIbansValue.is, selectedAccountsIbansValue)} +
+

Allowed actions:

+

Read account details

+

Read account balances

+

Read transactions

-
- -

2) Read account balances of:

-
- {generateCheckboxes("canReadBalancesIbans", canReadBalancesIbans, selectedBalancesIbansValue.is, selectedBalancesIbansValue)} +
+

Accounts:

+
+ {generateCheckboxes("canReadAccountsIbans", userIbans.toList, selectedAccountsIbansValue.is, selectedAccountsIbansValue)} +
+
-
- -

3) Read transactions of:

-
- {generateCheckboxes("canReadTransactionsIbans", canReadTransactionsIbans, selectedTransactionsIbansValue.is, selectedTransactionsIbansValue)} -
-
- -

This consent will end on date - {json.validUntil} - .

+

This consent will end on date {json.validUntil}.

I understand that I can revoke this consent at any time.

) & { @@ -315,14 +325,17 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 * Handles the confirmation of a consent request. */ private def confirmConsentRequestProcess() = { - if (selectedAccountsIbansValue.is.isEmpty && - selectedBalancesIbansValue.is.isEmpty && - selectedTransactionsIbansValue.is.isEmpty) { + if (selectedAccountsIbansValue.is.isEmpty) { S.error(s"Please select at least 1 account") } else { val consentId = ObpS.param("CONSENT_ID") openOr ("") if (updateConsentPayloadValue.is) { - updateConsent(consentId, selectedAccountsIbansValue.is.toList, selectedBalancesIbansValue.is.toList, selectedTransactionsIbansValue.is.toList) + updateConsent( + consentId, + selectedAccountsIbansValue.is.toList, + accessBalancesDefinedVar.is, + accessTransactionsDefinedVar.is + ) } S.redirectTo( s"/confirm-bg-consent-request-sca?CONSENT_ID=${consentId}" From 9f6707e28a4e9d052522f259e4e8cedf9d053605 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 25 Feb 2025 13:54:00 +0100 Subject: [PATCH 09/14] docfix/Remove non standard chars --- .../main/scala/code/api/util/CertificateVerifier.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala index 848c4efc5..b61053b9e 100644 --- a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala +++ b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala @@ -40,10 +40,10 @@ object CertificateVerifier extends MdcLoggable { trustStore } match { case Success(store) => - logger.info(s"✅ Loaded trust store from: $trustStorePath") + logger.info(s"Loaded trust store from: $trustStorePath") Some(store) case Failure(e) => - logger.info(s"❌ Failed to load trust store: ${e.getMessage}") + logger.info(s"Failed to load trust store: ${e.getMessage}") None } } @@ -145,11 +145,11 @@ object CertificateVerifier extends MdcLoggable { pemCertificate.foreach { pem => val isValid = validateCertificate(pem) - logger.info(s"✅ Certificate verification result: $isValid") + logger.info(s"Certificate verification result: $isValid") } loadTrustStore().foreach { trustStore => - logger.info(s"🔹 Trust Store contains ${trustStore.size()} certificates.") + logger.info(s"Trust Store contains ${trustStore.size()} certificates.") } } } From d9f8c06096899df48c1868d4d10fddd4da9d6c47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 25 Feb 2025 15:42:23 +0100 Subject: [PATCH 10/14] docfix/Remove non standard chars 2 --- obp-api/src/main/scala/code/api/util/CertificateVerifier.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala index b61053b9e..1773cab7e 100644 --- a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala +++ b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala @@ -133,7 +133,7 @@ object CertificateVerifier extends MdcLoggable { } match { case Success(pem) => Some(pem) case Failure(exception) => - println(s"❌ Failed to load PEM certificate from file: ${exception.getMessage}") + logger.error(s"Failed to load PEM certificate from file: ${exception.getMessage}") None } } From 0ba5b7b4934d056326ce8c5616cc473bff7d4efc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 25 Feb 2025 15:43:37 +0100 Subject: [PATCH 11/14] docfix/Remove non standard chars 3 --- .../src/main/scala/code/api/util/CertificateVerifier.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala index 1773cab7e..adbdd56ae 100644 --- a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala +++ b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala @@ -91,15 +91,15 @@ object CertificateVerifier extends MdcLoggable { val validator = CertPathValidator.getInstance("PKIX") validator.validate(certPath, pkixParams) - logger.info("✅ Certificate is valid and trusted.") + logger.info("Certificate is valid and trusted.") true } match { case Success(_) => true case Failure(e: CertPathValidatorException) => - logger.info(s"❌ Certificate validation failed: ${e.getMessage}") + logger.info(s"Certificate validation failed: ${e.getMessage}") false case Failure(e) => - logger.info(s"❌ Error: ${e.getMessage}") + logger.info(s"Error: ${e.getMessage}") false } } From f9effa8a6e99fbdcf6f4ed7bca3350740ae30568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 25 Feb 2025 15:45:06 +0100 Subject: [PATCH 12/14] docfix/Remove non standard chars 4 --- obp-api/src/main/scala/code/api/util/CertificateVerifier.scala | 2 -- 1 file changed, 2 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala index adbdd56ae..9204d08c9 100644 --- a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala +++ b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala @@ -19,8 +19,6 @@ object CertificateVerifier extends MdcLoggable { * This function: * - Reads the trust store password from the application properties (`truststore.path.tpp_signature`). * - Uses Java's `KeyStore` class to load the certificates. - * - If successful, logs `✅ Loaded trust store from: path`. - * - If it fails, logs `❌ Failed to load trust store: error message`. * * @return An `Option[KeyStore]` containing the loaded trust store, or `None` if loading fails. */ From c667f79b222a67515bb21247c00cdf6a079a9079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 25 Feb 2025 15:48:40 +0100 Subject: [PATCH 13/14] docfix/Remove personal path value --- obp-api/src/main/scala/code/api/util/CertificateVerifier.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala index 9204d08c9..83a99fdcd 100644 --- a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala +++ b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala @@ -137,8 +137,7 @@ object CertificateVerifier extends MdcLoggable { } def main(args: Array[String]): Unit = { - // val certificatePath = "/path/to/certificate.pem" - val certificatePath = "/home/marko/Downloads/BerlinGroupSigning/certificate.pem" + val certificatePath = "/path/to/certificate.pem" val pemCertificate = loadPemCertificateFromFile(certificatePath) pemCertificate.foreach { pem => From 3eabc5903cfdda343569c76890e331374317747e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 26 Feb 2025 14:07:28 +0100 Subject: [PATCH 14/14] docfix/Add some comments --- obp-api/src/main/scala/code/api/util/CertificateVerifier.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala index 83a99fdcd..cefb24abe 100644 --- a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala +++ b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala @@ -137,6 +137,7 @@ object CertificateVerifier extends MdcLoggable { } def main(args: Array[String]): Unit = { + // change the following path if using this function to test on your localhost val certificatePath = "/path/to/certificate.pem" val pemCertificate = loadPemCertificateFromFile(certificatePath)