feature/Improve error message at function createBerlinGroupConsentJWT

This commit is contained in:
Marko Milić 2025-07-04 12:14:43 +02:00
parent 927ba2c3af
commit 5240d47ec7
3 changed files with 84 additions and 77 deletions

View File

@ -80,6 +80,7 @@ object BerlinGroupError {
case "400" if message.contains("OBP-35001") => "CONSENT_UNKNOWN"
case "403" if message.contains("OBP-35001") => "CONSENT_UNKNOWN"
case "400" if message.contains("OBP-50200") => "RESOURCE_UNKNOWN"
case "404" if message.contains("OBP-30076") => "RESOURCE_UNKNOWN"
case "404" if message.contains("OBP-40001") => "RESOURCE_UNKNOWN"

View File

@ -739,89 +739,95 @@ object Consent extends MdcLoggable {
callContext: Option[CallContext]): Future[Box[String]] = {
val currentTimeInSeconds = System.currentTimeMillis / 1000
val validUntilTimeInSeconds = validUntil match {
case Some(date) => date.getTime() / 1000
case _ => currentTimeInSeconds
}
// Write Consent's Auth Context to the DB
user map { u =>
val validUntilTimeInSeconds = validUntil.map(_.getTime / 1000).getOrElse(currentTimeInSeconds)
// Write Consent's Auth Context to DB
user.foreach { u =>
val authContexts = UserAuthContextProvider.userAuthContextProvider.vend.getUserAuthContextsBox(u.userId)
.map(_.map(i => BasicUserAuthContext(i.key, i.value)))
ConsentAuthContextProvider.consentAuthContextProvider.vend.createOrUpdateConsentAuthContexts(consentId, authContexts.getOrElse(Nil))
}
// 1. Add access
// Helper to get ConsentView or fail box
def getConsentView(ibanOpt: Option[String], viewId: String): Future[Box[ConsentView]] = {
val iban = ibanOpt.getOrElse("")
Connector.connector.vend.getBankAccountByIban(iban, callContext).map { bankAccount =>
logger.debug(s"createBerlinGroupConsentJWT.bankAccount: $bankAccount")
val error = s"${InvalidConnectorResponse} IBAN: $iban ${handleBox(bankAccount._1)}"
bankAccount._1 match {
case Full(acc) =>
Full(ConsentView(
bank_id = acc.bankId.value,
account_id = acc.accountId.value,
view_id = viewId,
None
))
case _ =>
ErrorUtil.apiFailureToBox(error, 400)(callContext)
}
}
}
// Prepare lists of future boxes
val allAccesses = consent.access.accounts.getOrElse(Nil) :::
consent.access.balances.getOrElse(Nil) ::: // Balances access implies and Account access as well
consent.access.transactions.getOrElse(Nil) // Transactions access implies and Account access as well
val accounts: List[Future[ConsentView]] = allAccesses.distinct map { account =>
Connector.connector.vend.getBankAccountByIban(account.iban.getOrElse(""), callContext) map { bankAccount =>
logger.debug(s"createBerlinGroupConsentJWT.accounts.bankAccount: $bankAccount")
val error = s"${InvalidConnectorResponse} IBAN: ${account.iban.getOrElse("")} ${handleBox(bankAccount._1)}"
ConsentView(
bank_id = bankAccount._1.map(_.bankId.value).getOrElse(""),
account_id = bankAccount._1.map(_.accountId.value).openOrThrowException(error),
view_id = Constant.SYSTEM_READ_ACCOUNTS_BERLIN_GROUP_VIEW_ID,
None
)
consent.access.balances.getOrElse(Nil) :::
consent.access.transactions.getOrElse(Nil)
val accounts: List[Future[Box[ConsentView]]] = allAccesses.distinct.map { account =>
getConsentView(account.iban, Constant.SYSTEM_READ_ACCOUNTS_BERLIN_GROUP_VIEW_ID)
}
val balances: List[Future[Box[ConsentView]]] = consent.access.balances.getOrElse(Nil).map { account =>
getConsentView(account.iban, Constant.SYSTEM_READ_BALANCES_BERLIN_GROUP_VIEW_ID)
}
val transactions: List[Future[Box[ConsentView]]] = consent.access.transactions.getOrElse(Nil).map { account =>
getConsentView(account.iban, Constant.SYSTEM_READ_TRANSACTIONS_BERLIN_GROUP_VIEW_ID)
}
// Collect optional headers
val headers = callContext.map(_.requestHeaders).getOrElse(Nil)
val tppRedirectUri = headers.find(_.name == RequestHeader.`TPP-Redirect-URI`)
val tppNokRedirectUri = headers.find(_.name == RequestHeader.`TPP-Nok-Redirect-URI`)
val xRequestId = headers.find(_.name == RequestHeader.`X-Request-ID`)
val psuDeviceId = headers.find(_.name == RequestHeader.`PSU-Device-ID`)
val psuIpAddress = headers.find(_.name == RequestHeader.`PSU-IP-Address`)
val psuGeoLocation = headers.find(_.name == RequestHeader.`PSU-Geo-Location`)
def sequenceBoxes[A](boxes: List[Box[A]]): Box[List[A]] = {
boxes.foldRight(Full(Nil): Box[List[A]]) { (box, acc) =>
for {
x <- box
xs <- acc
} yield x :: xs
}
}
val balances: List[Future[ConsentView]] = consent.access.balances.getOrElse(Nil) map { account =>
Connector.connector.vend.getBankAccountByIban(account.iban.getOrElse(""), callContext) map { bankAccount =>
logger.debug(s"createBerlinGroupConsentJWT.balances.bankAccount: $bankAccount")
val error = s"${InvalidConnectorResponse} IBAN: ${account.iban.getOrElse("")} ${handleBox(bankAccount._1)}"
ConsentView(
bank_id = bankAccount._1.map(_.bankId.value).getOrElse(""),
account_id = bankAccount._1.map(_.accountId.value).openOrThrowException(error),
view_id = Constant.SYSTEM_READ_BALANCES_BERLIN_GROUP_VIEW_ID,
None
)
}
}
val transactions: List[Future[ConsentView]] = consent.access.transactions.getOrElse(Nil) map { account =>
Connector.connector.vend.getBankAccountByIban(account.iban.getOrElse(""), callContext) map { bankAccount =>
logger.debug(s"createBerlinGroupConsentJWT.transactions.bankAccount: $bankAccount")
val error = s"${InvalidConnectorResponse} IBAN: ${account.iban.getOrElse("")} ${handleBox(bankAccount._1)}"
ConsentView(
bank_id = bankAccount._1.map(_.bankId.value).getOrElse(""),
account_id = bankAccount._1.map(_.accountId.value).openOrThrowException(error),
view_id = Constant.SYSTEM_READ_TRANSACTIONS_BERLIN_GROUP_VIEW_ID,
None
)
}
}
val tppRedirectUri: Option[HTTPParam] = callContext.map(_.requestHeaders).getOrElse(Nil).find(_.name == RequestHeader.`TPP-Redirect-URI`)
val tppNokRedirectUri: Option[HTTPParam] = callContext.map(_.requestHeaders).getOrElse(Nil).find(_.name == RequestHeader.`TPP-Nok-Redirect-URI`)
val xRequestId: Option[HTTPParam] = callContext.map(_.requestHeaders).getOrElse(Nil).find(_.name == RequestHeader.`X-Request-ID`)
val psuDeviceId: Option[HTTPParam] = callContext.map(_.requestHeaders).getOrElse(Nil).find(_.name == RequestHeader.`PSU-Device-ID`)
val psuIpAddress: Option[HTTPParam] = callContext.map(_.requestHeaders).getOrElse(Nil).find(_.name == RequestHeader.`PSU-IP-Address`)
val psuGeoLocation: Option[HTTPParam] = callContext.map(_.requestHeaders).getOrElse(Nil).find(_.name == RequestHeader.`PSU-Geo-Location`)
Future.sequence(accounts ::: balances ::: transactions) map { views =>
// Combine and build final JWT
Future.sequence(accounts ::: balances ::: transactions).map { listOfBoxes =>
sequenceBoxes(listOfBoxes).map { views =>
val json = ConsentJWT(
createdByUserId = user.map(_.userId).getOrElse(""),
sub = APIUtil.generateUUID(),
iss = Constant.HostName,
aud = consumerId.getOrElse(""),
jti = consentId,
iat = currentTimeInSeconds,
nbf = currentTimeInSeconds,
exp = validUntilTimeInSeconds,
request_headers = tppRedirectUri.toList :::
tppNokRedirectUri.toList :::
xRequestId.toList :::
psuDeviceId.toList :::
psuIpAddress.toList :::
psuGeoLocation.toList,
name = None,
email = None,
entitlements = Nil,
views = views,
access = Some(consent.access)
)
implicit val formats = CustomJsonFormats.formats
val jwtPayloadAsJson = compactRender(Extraction.decompose(json))
val jwtClaims: JWTClaimsSet = JWTClaimsSet.parse(jwtPayloadAsJson)
Full(CertificateUtil.jwtWithHmacProtection(jwtClaims, secret))
createdByUserId = user.map(_.userId).getOrElse(""),
sub = APIUtil.generateUUID(),
iss = Constant.HostName,
aud = consumerId.getOrElse(""),
jti = consentId,
iat = currentTimeInSeconds,
nbf = currentTimeInSeconds,
exp = validUntilTimeInSeconds,
request_headers = List(
tppRedirectUri, tppNokRedirectUri, xRequestId, psuDeviceId, psuIpAddress, psuGeoLocation
).flatten,
name = None,
email = None,
entitlements = Nil,
views = views,
access = Some(consent.access)
)
implicit val formats = CustomJsonFormats.formats
val jwtPayloadAsJson = compactRender(Extraction.decompose(json))
val jwtClaims: JWTClaimsSet = JWTClaimsSet.parse(jwtPayloadAsJson)
CertificateUtil.jwtWithHmacProtection(jwtClaims, secret)
}
}
}
def updateAccountAccessOfBerlinGroupConsentJWT(access: ConsentAccessJson,

View File

@ -21,13 +21,13 @@ object ErrorUtil {
)
}
def apiFailureToBox(errorMessage: String, httpCode: Int)(cc: Option[CallContext]): Box[Nothing] = {
def apiFailureToBox[T](errorMessage: String, httpCode: Int)(cc: Option[CallContext]): Box[T] = {
val apiFailure = APIFailureNewStyle(
failMsg = errorMessage,
failCode = httpCode,
ccl = cc.map(_.toLight)
)
val failureBox = Empty ~> apiFailure
val failureBox: Box[T] = Empty ~> apiFailure
fullBoxOrException(failureBox)
}