From d10fc11132cc197da1b92f4579d3e8d31ac4203f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Mon, 24 Jun 2024 09:59:32 +0200 Subject: [PATCH 01/11] feature/Tweak function createGetConsentResponseJson --- .../berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala index 5683e417b6..096de0a477 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala @@ -545,11 +545,11 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats { GetConsentResponseJson( access = access, recurringIndicator = createdConsent.recurringIndicator, - validUntil = new SimpleDateFormat(DateWithDay).format(createdConsent.validUntil), + validUntil = if(createdConsent.validUntil == null) null else new SimpleDateFormat(DateWithDay).format(createdConsent.validUntil), frequencyPerDay = createdConsent.frequencyPerDay, combinedServiceIndicator= createdConsent.combinedServiceIndicator, - lastActionDate= new SimpleDateFormat(DateWithDay).format(createdConsent.lastActionDate), - consentStatus= createdConsent.status.toLowerCase() + lastActionDate = if(createdConsent.lastActionDate == null) null else new SimpleDateFormat(DateWithDay).format(createdConsent.lastActionDate), + consentStatus = createdConsent.status.toLowerCase() ) } From e84db053bf30aae28044c79d57333f74d5932b88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 26 Jun 2024 10:40:03 +0200 Subject: [PATCH 02/11] feature/Introduce function comparePemX509Certificates --- obp-api/src/main/scala/code/api/OAuth2.scala | 16 +++------ .../scala/code/api/util/CertificateUtil.scala | 34 +++++++++++++++++++ .../scala/code/api/util/ConsentUtil.scala | 14 ++++---- 3 files changed, 46 insertions(+), 18 deletions(-) diff --git a/obp-api/src/main/scala/code/api/OAuth2.scala b/obp-api/src/main/scala/code/api/OAuth2.scala index 396cd89e19..b68644e5bb 100644 --- a/obp-api/src/main/scala/code/api/OAuth2.scala +++ b/obp-api/src/main/scala/code/api/OAuth2.scala @@ -28,8 +28,9 @@ package code.api import java.net.URI import java.util + import code.api.util.ErrorMessages._ -import code.api.util.{APIUtil, CallContext, JwtUtil} +import code.api.util.{APIUtil, CallContext, CertificateUtil, JwtUtil} import code.consumer.Consumers import code.consumer.Consumers.consumers import code.loginattempts.LoginAttempt @@ -162,8 +163,8 @@ object OAuth2Login extends RestHelper with MdcLoggable { // hydra update client endpoint have bug, So here delete and create to do update hydraAdmin.deleteOAuth2Client(clientId) hydraAdmin.createOAuth2Client(oAuth2Client) - } else if(stringNotEq(certInConsumer, cert)) { - // Cannot match the value from PSD2-CERT header and the database value Consumer.clientCertificate + } else if(!CertificateUtil.comparePemX509Certificates(certInConsumer, cert)) { + // Cannot mat.ch the value from PSD2-CERT header and the database value Consumer.clientCertificate logger.debug("Cert in Consumer: " + certInConsumer) logger.debug("Cert in Request: " + cert) logger.debug(s"Token: $value") @@ -208,15 +209,6 @@ object OAuth2Login extends RestHelper with MdcLoggable { applyRules(value, cc) } - /** - * check whether two string equal, ignore line break type - * @param str1 - * @param str2 - * @return true if two string are different - */ - private def stringNotEq(str1: String, str2: String): Boolean = - str1.trim != str2.trim && - str1.trim.replace("\r\n", "\n") != str2.trim.replace("\r\n", "\n") } trait OAuth2Util { diff --git a/obp-api/src/main/scala/code/api/util/CertificateUtil.scala b/obp-api/src/main/scala/code/api/util/CertificateUtil.scala index 9b170461da..cfa38eae20 100644 --- a/obp-api/src/main/scala/code/api/util/CertificateUtil.scala +++ b/obp-api/src/main/scala/code/api/util/CertificateUtil.scala @@ -225,6 +225,40 @@ object CertificateUtil extends MdcLoggable { jwtParsed.getJWTClaimsSet } + // Remove all whitespace characters including spaces, tabs, newlines, and carriage returns + def normalizePemX509Certificate(pem: String): String = { + val pemHeader = "-----BEGIN CERTIFICATE-----" + val pemFooter = "-----END CERTIFICATE-----" + + def extractContent(pem: String): Option[String] = { + val start = pem.indexOf(pemHeader) + val end = pem.indexOf(pemFooter) + + if (start >= 0 && end > start) { + Some(pem.substring(start + pemHeader.length, end)) + } else { + None + } + } + + extractContent(pem).map { content => // Extract content from PEM representation of X509 certificate + val normalizedContent = content.replaceAll("\\s+", "") + s"$pemHeader$normalizedContent$pemFooter" + }.getOrElse(pem) // In case the extraction cannot be done default the input value we try to normalize + } + + def comparePemX509Certificates(pem1: String, pem2: String): Boolean = { + val normalizedPem1 = normalizePemX509Certificate(pem1) + val normalizedPem2 = normalizePemX509Certificate(pem2) + + val result = normalizedPem1 == normalizedPem2 + if(!result) { + logger.debug(s"normalizedPem1: ${normalizedPem1}") + logger.debug(s"normalizedPem2: ${normalizedPem2}") + } + result + } + def main(args: Array[String]): Unit = { diff --git a/obp-api/src/main/scala/code/api/util/ConsentUtil.scala b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala index e78704fe30..5e565ec37b 100644 --- a/obp-api/src/main/scala/code/api/util/ConsentUtil.scala +++ b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala @@ -126,12 +126,14 @@ object Consent extends MdcLoggable { def getCurrentConsumerViaMtls(callContext: CallContext): Box[Consumer] = { val clientCert: String = APIUtil.`getPSD2-CERT`(callContext.requestHeaders) .getOrElse(SecureRandomUtil.csprng.nextLong().toString) - def removeBreakLines(input: String) = input - .replace("\n", "") - .replace("\r", "") - Consumers.consumers.vend.getConsumerByPemCertificate(clientCert).or( - Consumers.consumers.vend.getConsumerByPemCertificate(removeBreakLines(clientCert)) - ) + + { // 1st search is via the original value + logger.debug(s"getConsumerByPemCertificate ${clientCert}") + Consumers.consumers.vend.getConsumerByPemCertificate(clientCert) + }.or { // 2nd search is via the original value we normalize + logger.debug(s"getConsumerByPemCertificate ${CertificateUtil.normalizePemX509Certificate(clientCert)}") + Consumers.consumers.vend.getConsumerByPemCertificate(CertificateUtil.normalizePemX509Certificate(clientCert)) + } } private def verifyHmacSignedJwt(jwtToken: String, c: MappedConsent): Boolean = { From 2b702895838c68923790695bea82e7fd93f3b09a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 26 Jun 2024 10:45:55 +0200 Subject: [PATCH 03/11] feature/Add endpoint getConsentByConsentIdViaConsumer v5.1.0 --- .../scala/code/api/v5_1_0/APIMethods510.scala | 42 ++++++++++++++++++- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala index aa7ffcc8bb..83ee9d7187 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala @@ -976,7 +976,7 @@ trait APIMethods510 { implementedInApiVersion, nameOf(getConsentByConsentId), "GET", - "/consumer/consents/CONSENT_ID", + "/user/consents/CONSENT_ID", "Get Consent By Consent Id", s""" | @@ -993,7 +993,7 @@ trait APIMethods510 { ), List(apiTagConsent, apiTagPSD2AIS, apiTagPsd2)) lazy val getConsentByConsentId: OBPEndpoint = { - case "consumer" :: "consents" :: consentId :: Nil JsonGet _ => { + case "user" :: "consents" :: consentId :: Nil JsonGet _ => { cc => implicit val ec = EndpointContext(Some(cc)) for { consent <- Future { Consents.consentProvider.vend.getConsentByConsentId(consentId)} map { @@ -1007,6 +1007,44 @@ trait APIMethods510 { } } } + + + staticResourceDocs += ResourceDoc( + getConsentByConsentIdViaConsumer, + implementedInApiVersion, + nameOf(getConsentByConsentIdViaConsumer), + "GET", + "/consumer/consents/CONSENT_ID", + "Get Consent By Consent Id", + s""" + | + |This endpoint gets the Consent By consent id. + | + |${authenticationRequiredMessage(true)} + | + """.stripMargin, + EmptyBody, + consentJsonV500, + List( + $UserNotLoggedIn, + UnknownError + ), + List(apiTagConsent, apiTagPSD2AIS, apiTagPsd2)) + lazy val getConsentByConsentIdViaConsumer: OBPEndpoint = { + case "consumer" :: "consents" :: consentId :: Nil JsonGet _ => { + cc => implicit val ec = EndpointContext(Some(cc)) + for { + consent <- Future { Consents.consentProvider.vend.getConsentByConsentId(consentId)} map { + unboxFullOrFail(_, cc.callContext, ConsentNotFound, 404) + } + _ <- Helper.booleanToFuture(failMsg = s"${consent.mConsumerId.get} != ${cc.consumer.map(_.consumerId.get).getOrElse("None")}", failCode = 404, cc = cc.callContext) { + consent.mConsumerId.get == cc.consumer.map(_.consumerId.get).getOrElse("None") + } + } yield { + (JSONFactory510.getConsentInfoJson(consent), HttpCode.`200`(cc)) + } + } + } staticResourceDocs += ResourceDoc( revokeConsentAtBank, From a2e17ba2bb087bdd4f1e976ae4e4b543281ba5b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Mon, 1 Jul 2024 14:26:04 +0200 Subject: [PATCH 04/11] docfix/Add how to run all tests via terminal --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 831a95511e..0617550840 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,10 @@ Run one test mvn -DwildcardSuites=code.api.directloginTest test +Run all test and save output to a file + + export MAVEN_OPTS="-Xss128m" && mvn clean test | tee obp-api-test-results.txt + ## Ubuntu If you use Ubuntu (or a derivate) and encrypted home directories (e.g. you have ~/.Private), you might run into the following error when the project is built: From 75cdea09b0c749458b8fe0ced4a28a7cf5bdd197 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Fri, 5 Jul 2024 14:24:37 +0200 Subject: [PATCH 05/11] feature/Tweak the path of endpoint getConsentByConsentId v5.1.0 --- obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala index 469cf35735..af03b356bf 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala @@ -976,7 +976,7 @@ trait APIMethods510 { implementedInApiVersion, nameOf(getConsentByConsentId), "GET", - "/user/consents/CONSENT_ID", + "/user/current/consents/CONSENT_ID", "Get Consent By Consent Id", s""" | @@ -993,7 +993,7 @@ trait APIMethods510 { ), List(apiTagConsent, apiTagPSD2AIS, apiTagPsd2)) lazy val getConsentByConsentId: OBPEndpoint = { - case "user" :: "consents" :: consentId :: Nil JsonGet _ => { + case "user" :: "current" :: "consents" :: consentId :: Nil JsonGet _ => { cc => implicit val ec = EndpointContext(Some(cc)) for { consent <- Future { Consents.consentProvider.vend.getConsentByConsentId(consentId)} map { From d32bf2796e2e3921689fa77d7279dae0321ddfee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Fri, 5 Jul 2024 14:43:27 +0200 Subject: [PATCH 06/11] feature/Tweak the path of endpoint getConsentByConsentIdViaConsumer v5.1.0 --- obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala index af03b356bf..37126451e6 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala @@ -1014,7 +1014,7 @@ trait APIMethods510 { implementedInApiVersion, nameOf(getConsentByConsentIdViaConsumer), "GET", - "/consumer/consents/CONSENT_ID", + "/consumer/current/consents/CONSENT_ID", "Get Consent By Consent Id", s""" | @@ -1031,7 +1031,7 @@ trait APIMethods510 { ), List(apiTagConsent, apiTagPSD2AIS, apiTagPsd2)) lazy val getConsentByConsentIdViaConsumer: OBPEndpoint = { - case "consumer" :: "consents" :: consentId :: Nil JsonGet _ => { + case "consumer" :: "current" :: "consents" :: consentId :: Nil JsonGet _ => { cc => implicit val ec = EndpointContext(Some(cc)) for { consent <- Future { Consents.consentProvider.vend.getConsentByConsentId(consentId)} map { From 9005c053f38dc287222765856dd0c897c82f494f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 10 Jul 2024 15:13:39 +0200 Subject: [PATCH 07/11] docfix/Put debug logging at ConsentUtil --- obp-api/src/main/scala/code/api/util/ConsentUtil.scala | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/obp-api/src/main/scala/code/api/util/ConsentUtil.scala b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala index 5e565ec37b..613313267f 100644 --- a/obp-api/src/main/scala/code/api/util/ConsentUtil.scala +++ b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala @@ -316,7 +316,9 @@ object Consent extends MdcLoggable { JwtUtil.getSignedPayloadAsJson(consentIdAsJwt) match { case Full(jsonAsString) => try { + logger.debug(s"Start of net.liftweb.json.parse(jsonAsString).extract[ConsentJWT]") val consent = net.liftweb.json.parse(jsonAsString).extract[ConsentJWT] + logger.debug(s"End of net.liftweb.json.parse(jsonAsString).extract[ConsentJWT]: $consent") checkConsent(consent, consentIdAsJwt, calContext) match { // Check is it Consent-JWT expired case (Full(true)) => // OK applyConsentRules(consent) @@ -371,7 +373,9 @@ object Consent extends MdcLoggable { JwtUtil.getSignedPayloadAsJson(consentAsJwt) match { case Full(jsonAsString) => try { + logger.debug(s"Start of net.liftweb.json.parse(jsonAsString).extract[ConsentJWT]") val consent = net.liftweb.json.parse(jsonAsString).extract[ConsentJWT] + logger.debug(s"End of net.liftweb.json.parse(jsonAsString).extract[ConsentJWT]: $consent") // Set Consumer into Call Context val consumer = getCurrentConsumerViaMtls(callContext) val updatedCallContext = callContext.copy(consumer = consumer) @@ -490,7 +494,9 @@ object Consent extends MdcLoggable { JwtUtil.getSignedPayloadAsJson(storedConsent.jsonWebToken) match { case Full(jsonAsString) => try { + logger.debug(s"Start of net.liftweb.json.parse(jsonAsString).extract[ConsentJWT]") val consent = net.liftweb.json.parse(jsonAsString).extract[ConsentJWT] + logger.debug(s"End of net.liftweb.json.parse(jsonAsString).extract[ConsentJWT]: $consent") checkConsent(consent, storedConsent.jsonWebToken, updatedCallContext) match { // Check is it Consent-JWT expired case (Full(true)) => // OK // Update MappedConsent.usesSoFarTodayCounter field From d9cf31fde52fb4b6a2da923b14f6ae006019eede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Thu, 11 Jul 2024 10:38:59 +0200 Subject: [PATCH 08/11] docfix/Put debug logging at ConsentUtil 2 --- obp-api/src/main/scala/code/api/util/ConsentUtil.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/ConsentUtil.scala b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala index 613313267f..d17cbd25cb 100644 --- a/obp-api/src/main/scala/code/api/util/ConsentUtil.scala +++ b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala @@ -316,7 +316,7 @@ object Consent extends MdcLoggable { JwtUtil.getSignedPayloadAsJson(consentIdAsJwt) match { case Full(jsonAsString) => try { - logger.debug(s"Start of net.liftweb.json.parse(jsonAsString).extract[ConsentJWT]") + logger.debug(s"Start of net.liftweb.json.parse(jsonAsString).extract[ConsentJWT]: $jsonAsString") val consent = net.liftweb.json.parse(jsonAsString).extract[ConsentJWT] logger.debug(s"End of net.liftweb.json.parse(jsonAsString).extract[ConsentJWT]: $consent") checkConsent(consent, consentIdAsJwt, calContext) match { // Check is it Consent-JWT expired @@ -373,7 +373,7 @@ object Consent extends MdcLoggable { JwtUtil.getSignedPayloadAsJson(consentAsJwt) match { case Full(jsonAsString) => try { - logger.debug(s"Start of net.liftweb.json.parse(jsonAsString).extract[ConsentJWT]") + logger.debug(s"Start of net.liftweb.json.parse(jsonAsString).extract[ConsentJWT]: $jsonAsString") val consent = net.liftweb.json.parse(jsonAsString).extract[ConsentJWT] logger.debug(s"End of net.liftweb.json.parse(jsonAsString).extract[ConsentJWT]: $consent") // Set Consumer into Call Context @@ -494,7 +494,7 @@ object Consent extends MdcLoggable { JwtUtil.getSignedPayloadAsJson(storedConsent.jsonWebToken) match { case Full(jsonAsString) => try { - logger.debug(s"Start of net.liftweb.json.parse(jsonAsString).extract[ConsentJWT]") + logger.debug(s"Start of net.liftweb.json.parse(jsonAsString).extract[ConsentJWT]: $jsonAsString") val consent = net.liftweb.json.parse(jsonAsString).extract[ConsentJWT] logger.debug(s"End of net.liftweb.json.parse(jsonAsString).extract[ConsentJWT]: $consent") checkConsent(consent, storedConsent.jsonWebToken, updatedCallContext) match { // Check is it Consent-JWT expired From 49348c9033127ff5e070b6de897b3c825f0618d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Thu, 11 Jul 2024 12:06:54 +0200 Subject: [PATCH 09/11] test/Fix failing Consent test --- obp-api/src/test/scala/code/api/v5_1_0/ConsentsTest.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obp-api/src/test/scala/code/api/v5_1_0/ConsentsTest.scala b/obp-api/src/test/scala/code/api/v5_1_0/ConsentsTest.scala index 4dc53a08ab..e6b3aeaa05 100644 --- a/obp-api/src/test/scala/code/api/v5_1_0/ConsentsTest.scala +++ b/obp-api/src/test/scala/code/api/v5_1_0/ConsentsTest.scala @@ -82,7 +82,7 @@ class ConsentsTest extends V510ServerSetup with PropsReset{ def getConsentRequestUrl(requestId:String) = (v5_1_0_Request / "consumer"/ "consent-requests"/requestId).GET<@(user1) def createConsentByConsentRequestIdEmail(requestId:String) = (v5_1_0_Request / "consumer"/ "consent-requests"/requestId/"EMAIL"/"consents").POST<@(user1) def getConsentByRequestIdUrl(requestId:String) = (v5_1_0_Request / "consumer"/ "consent-requests"/requestId/"consents").GET<@(user1) - def getConsentByIdUrl(requestId:String) = (v5_1_0_Request / "consumer" / "consents" / requestId ).GET<@(user1) + def getConsentByIdUrl(requestId:String) = (v5_1_0_Request / "consumer" / "current" / "consents" / requestId ).GET<@(user1) def revokeConsentUrl(consentId: String) = (v5_1_0_Request / "banks" / bankId / "consents" / consentId).DELETE feature(s"test $ApiEndpoint6 version $VersionOfApi - Unauthorized access") { From d33312aec30b894e25f18ce2e84d022d8f490fb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Fri, 12 Jul 2024 08:56:23 +0200 Subject: [PATCH 10/11] bugfix/Google registration on API Sandbox does not work --- obp-api/src/main/scala/code/api/openidconnect.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/obp-api/src/main/scala/code/api/openidconnect.scala b/obp-api/src/main/scala/code/api/openidconnect.scala index f05adb7eb3..b341f9d8d8 100644 --- a/obp-api/src/main/scala/code/api/openidconnect.scala +++ b/obp-api/src/main/scala/code/api/openidconnect.scala @@ -183,7 +183,8 @@ object OpenIdConnect extends OBPRestHelper with MdcLoggable { } private def extractParams(s: S): (String, String, String) = { - val code = ObpS.param("code") + // TODO Figure out why ObpS does not contain response parameter code + val code = s.param("code") val state = ObpS.param("state") val sessionState = OpenIDConnectSessionState.get (code.getOrElse(""), state.getOrElse("0"), sessionState.map(_.toString).getOrElse("1")) From 61727b35797f654a61d815df8dc67861159ca934 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Fri, 12 Jul 2024 09:05:38 +0200 Subject: [PATCH 11/11] feature/Bump scalatest-maven-plugin to 2.2.0 --- pom.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a0fb316aae..270f475b7b 100644 --- a/pom.xml +++ b/pom.xml @@ -163,10 +163,11 @@ + org.scalatest scalatest-maven-plugin - 2.0.0 + 2.2.0