Skip to content

Commit

Permalink
Merge pull request #2409 from constantine2nd/develop
Browse files Browse the repository at this point in the history
Consents
  • Loading branch information
simonredfern authored Jul 12, 2024
2 parents 3eb9526 + 61727b3 commit 550ce24
Show file tree
Hide file tree
Showing 9 changed files with 105 additions and 26 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
17 changes: 5 additions & 12 deletions obp-api/src/main/scala/code/api/OAuth2.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ TESOBE (http://www.tesobe.com/)
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
Expand Down Expand Up @@ -161,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")
Expand Down Expand Up @@ -207,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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
)
}

Expand Down
3 changes: 2 additions & 1 deletion obp-api/src/main/scala/code/api/openidconnect.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
34 changes: 34 additions & 0 deletions obp-api/src/main/scala/code/api/util/CertificateUtil.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
20 changes: 14 additions & 6 deletions obp-api/src/main/scala/code/api/util/ConsentUtil.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -314,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]: $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
case (Full(true)) => // OK
applyConsentRules(consent)
Expand Down Expand Up @@ -369,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]: $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
val consumer = getCurrentConsumerViaMtls(callContext)
val updatedCallContext = callContext.copy(consumer = consumer)
Expand Down Expand Up @@ -488,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]: $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
case (Full(true)) => // OK
// Update MappedConsent.usesSoFarTodayCounter field
Expand Down
42 changes: 40 additions & 2 deletions obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala
Original file line number Diff line number Diff line change
Expand Up @@ -976,7 +976,7 @@ trait APIMethods510 {
implementedInApiVersion,
nameOf(getConsentByConsentId),
"GET",
"/consumer/consents/CONSENT_ID",
"/user/current/consents/CONSENT_ID",
"Get Consent By Consent Id",
s"""
|
Expand All @@ -993,7 +993,7 @@ trait APIMethods510 {
),
List(apiTagConsent, apiTagPSD2AIS, apiTagPsd2))
lazy val getConsentByConsentId: OBPEndpoint = {
case "consumer" :: "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 {
Expand All @@ -1007,6 +1007,44 @@ trait APIMethods510 {
}
}
}


staticResourceDocs += ResourceDoc(
getConsentByConsentIdViaConsumer,
implementedInApiVersion,
nameOf(getConsentByConsentIdViaConsumer),
"GET",
"/consumer/current/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" :: "current" :: "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,
Expand Down
2 changes: 1 addition & 1 deletion obp-api/src/test/scala/code/api/v5_1_0/ConsentsTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
3 changes: 2 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -163,10 +163,11 @@
</execution>
</executions>
</plugin>
<!-- https://mvnrepository.com/artifact/org.scalatest/scalatest-maven-plugin -->
<plugin>
<groupId>org.scalatest</groupId>
<artifactId>scalatest-maven-plugin</artifactId>
<version>2.0.0</version>
<version>2.2.0</version>
</plugin>
</plugins>
</pluginManagement>
Expand Down

0 comments on commit 550ce24

Please sign in to comment.