Skip to content
6 changes: 6 additions & 0 deletions obp-api/src/main/resources/props/sample.props.template
Original file line number Diff line number Diff line change
Expand Up @@ -1160,12 +1160,18 @@ default_auth_context_update_request_key=CUSTOMER_NUMBER
## Berlin Group Create Consent Frequency per Day Upper Limit
#berlin_group_frequency_per_day_upper_limit = 4

## Berlin Group Create Consent ASPSP-SCA-Approach response header value
#berlin_group_aspsp_sca_approach = redirect

# Support multiple brands on one instance. Note this needs checking on a clustered environment
#brands_enabled=false

# Support removing the app type checkbox during consumer registration
#consumer_registration.display_app_type=true

# Default logo URL during of consumer
#consumer_default_logo_url=

# if set this props, we can automatically grant the Entitlements required to use all the Dynamic Endpoint roles belonging
# to the bank_ids (Spaces) the User has access to via their validated email domain. Entitlements are generated /refreshed
# both following manual login and Direct Login token generation (POST).
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package code.api.builder.AccountInformationServiceAISApi

import java.text.SimpleDateFormat
import code.api.APIFailureNewStyle
import code.api.Constant.{SYSTEM_READ_ACCOUNTS_BERLIN_GROUP_VIEW_ID, SYSTEM_READ_BALANCES_BERLIN_GROUP_VIEW_ID, SYSTEM_READ_TRANSACTIONS_BERLIN_GROUP_VIEW_ID}
import code.api.berlin.group.ConstantsBG
Expand All @@ -13,7 +12,7 @@ import code.api.util.ApiTag._
import code.api.util.ErrorMessages._
import code.api.util.NewStyle.HttpCode
import code.api.util.newstyle.BalanceNewStyle
import code.api.util.{APIUtil, ApiTag, CallContext, Consent, ExampleValue, NewStyle}
import code.api.util._
import code.bankconnectors.Connector
import code.consent.{ConsentStatus, Consents}
import code.context.{ConsentAuthContextProvider, UserAuthContextProvider}
Expand All @@ -24,16 +23,14 @@ import code.views.Views
import com.github.dwickern.macros.NameOf.nameOf
import com.openbankproject.commons.ExecutionContext.Implicits.global
import com.openbankproject.commons.model._
import com.openbankproject.commons.model.enums.{ChallengeType, StrongCustomerAuthentication, StrongCustomerAuthenticationStatus, SuppliedAnswerType}
import com.openbankproject.commons.util.ApiVersion
import com.openbankproject.commons.model.enums.{ChallengeType, StrongCustomerAuthenticationStatus, SuppliedAnswerType}
import net.liftweb
import net.liftweb.common.{Empty, Full}
import net.liftweb.http.js.JE.JsRaw
import net.liftweb.http.rest.RestHelper
import net.liftweb
import net.liftweb.json
import net.liftweb.json._

import scala.collection.immutable.Nil
import scala.collection.mutable.ArrayBuffer
import scala.concurrent.Future

Expand Down Expand Up @@ -160,11 +157,28 @@ recurringIndicator:
consentJson <- NewStyle.function.tryons(failMsg, 400, callContext) {
json.extract[PostConsentJson]
}
_ <- Helper.booleanToFuture(failMsg = BerlinGroupConsentAccessIsEmpty, cc=callContext) {
consentJson.access.accounts.isDefined ||
consentJson.access.balances.isDefined ||
consentJson.access.transactions.isDefined

_ <- if (consentJson.access.availableAccounts.isDefined) {
for {
_ <- Helper.booleanToFuture(failMsg = BerlinGroupConsentAccessAvailableAccounts, cc = callContext) {
consentJson.access.availableAccounts.contains("allAccounts")
}
_ <- Helper.booleanToFuture(failMsg = BerlinGroupConsentAccessRecurringIndicator, cc = callContext) {
!consentJson.recurringIndicator
}
_ <- Helper.booleanToFuture(failMsg = BerlinGroupConsentAccessFrequencyPerDay, cc = callContext) {
consentJson.frequencyPerDay == 1
}
} yield Full(())
} else {
Helper.booleanToFuture(
failMsg = BerlinGroupConsentAccessIsEmpty, cc = callContext) {
consentJson.access.accounts.isDefined ||
consentJson.access.balances.isDefined ||
consentJson.access.transactions.isDefined
}
}

upperLimit = APIUtil.getPropsAsIntValue("berlin_group_frequency_per_day_upper_limit", 4)
_ <- Helper.booleanToFuture(failMsg = FrequencyPerDayError, cc=callContext) {
consentJson.frequencyPerDay > 0 && consentJson.frequencyPerDay <= upperLimit
Expand Down Expand Up @@ -246,11 +260,16 @@ recurringIndicator:
case "consents" :: consentId :: Nil JsonDelete _ => {
cc =>
for {
(Full(user), callContext) <- authenticatedAccess(cc)
(_, callContext) <- applicationAccess(cc)
_ <- passesPsd2Aisp(callContext)
_ <- Future(Consents.consentProvider.vend.getConsentByConsentId(consentId)) map {
consent <- Future(Consents.consentProvider.vend.getConsentByConsentId(consentId)) map {
unboxFullOrFail(_, callContext, ConsentNotFound, 403)
}
consumerIdFromConsent = consent.mConsumerId.get
consumerIdFromCurrentCall = callContext.map(_.consumer.map(_.consumerId.get).getOrElse("None")).getOrElse("None")
_ <- Helper.booleanToFuture(failMsg = s"$ConsentNotFound $consumerIdFromConsent != $consumerIdFromCurrentCall", failCode = 403, cc = cc.callContext) {
consumerIdFromConsent == consumerIdFromCurrentCall
}
_ <- Future(Consents.consentProvider.vend.revokeBerlinGroupConsent(consentId)) map {
i => connectorEmptyResponse(i, callContext)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ object BgSpecValidation {

if (date.isBefore(today)) {
Left(s"$InvalidDateFormat The `validUntil` date ($dateStr) cannot be in the past!")
} else if (date.isAfter(MaxValidDays)) {
} else if (date.isEqual(MaxValidDays) || date.isAfter(MaxValidDays)) {
Left(s"$InvalidDateFormat The `validUntil` date ($dateStr) exceeds the maximum allowed period of 180 days (until $MaxValidDays).")
} else {
Right(date) // Valid date
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ object OBP_BERLIN_GROUP_1_3_Alias extends OBPRestHelper with MdcLoggable with Sc

override val allResourceDocs: ArrayBuffer[ResourceDoc] = if(berlinGroupV13AliasPath.nonEmpty){
OBP_BERLIN_GROUP_1_3.allResourceDocs.map(resourceDoc => resourceDoc.copy(
implementedInApiVersion = apiVersion,
implementedInApiVersion = apiVersion.copy(apiStandard = resourceDoc.implementedInApiVersion.apiStandard),
))
} else ArrayBuffer.empty[ResourceDoc]

Expand Down
1 change: 1 addition & 0 deletions obp-api/src/main/scala/code/api/constant/constant.scala
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ object RequestHeader {
final lazy val `If-Modified-Since` = "If-Modified-Since"
}
object ResponseHeader {
final lazy val `ASPSP-SCA-Approach` = "ASPSP-SCA-Approach" // Berlin Group
final lazy val `Correlation-Id` = "Correlation-Id"
final lazy val `WWW-Authenticate` = "WWW-Authenticate"
final lazy val ETag = "ETag"
Expand Down
17 changes: 15 additions & 2 deletions obp-api/src/main/scala/code/api/util/APIUtil.scala
Original file line number Diff line number Diff line change
Expand Up @@ -449,8 +449,9 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{

private def getHeadersNewStyle(cc: Option[CallContextLight]) = {
CustomResponseHeaders(
getGatewayLoginHeader(cc).list :::
getRateLimitHeadersNewStyle(cc).list :::
getGatewayLoginHeader(cc).list :::
getRequestHeadersBerlinGroup(cc).list :::
getRateLimitHeadersNewStyle(cc).list :::
getPaginationHeadersNewStyle(cc).list :::
getRequestHeadersToMirror(cc).list :::
getRequestHeadersToEcho(cc).list
Expand Down Expand Up @@ -556,6 +557,18 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
CustomResponseHeaders(Nil)
}
}

def getRequestHeadersBerlinGroup(callContext: Option[CallContextLight]): CustomResponseHeaders = {
val aspspScaApproach = getPropsValue("berlin_group_aspsp_sca_approach", defaultValue = "redirect")
callContext match {
case Some(cc) if cc.url.contains(ConstantsBG.berlinGroupVersion1.urlPrefix) && cc.url.endsWith("/consents") =>
CustomResponseHeaders(List(
(ResponseHeader.`ASPSP-SCA-Approach`, aspspScaApproach)
))
case _ =>
CustomResponseHeaders(Nil)
}
}
/**
*
* @param jwt is a JWT value extracted from GatewayLogin Authorization Header.
Expand Down
3 changes: 3 additions & 0 deletions obp-api/src/main/scala/code/api/util/BerlinGroupError.scala
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ object BerlinGroupError {
case "400" if message.contains("OBP-20252") => "FORMAT_ERROR"
case "400" if message.contains("OBP-20251") => "FORMAT_ERROR"
case "400" if message.contains("OBP-20088") => "FORMAT_ERROR"
case "400" if message.contains("OBP-20089") => "FORMAT_ERROR"
case "400" if message.contains("OBP-20090") => "FORMAT_ERROR"
case "400" if message.contains("OBP-20091") => "FORMAT_ERROR"

case "429" if message.contains("OBP-10018") => "ACCESS_EXCEEDED"
case _ => code
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,8 @@ object BerlinGroupSigning extends MdcLoggable {
developerEmail = extractedEmail,
redirectURL = None,
createdByUserId = None,
certificate = None
certificate = None,
logoUrl = APIUtil.getPropsValue("consumer_default_logo_url")
)

// Set or update certificate
Expand Down
62 changes: 62 additions & 0 deletions obp-api/src/main/scala/code/api/util/ConsentUtil.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package code.api.util

import code.accountholders.AccountHolders
import code.api.berlin.group.ConstantsBG

import java.text.SimpleDateFormat
Expand All @@ -20,6 +21,7 @@ import code.consumer.Consumers
import code.context.{ConsentAuthContextProvider, UserAuthContextProvider}
import code.entitlement.Entitlement
import code.model.Consumer
import code.model.dataAccess.BankAccountRouting
import code.scheduler.ConsentScheduler.logger
import code.users.Users
import code.util.Helper.MdcLoggable
Expand Down Expand Up @@ -873,6 +875,66 @@ object Consent extends MdcLoggable {
}
}
}
def updateViewsOfBerlinGroupConsentJWT(user: User,
consent: MappedConsent,
callContext: Option[CallContext]): Future[Box[MappedConsent]] = {
implicit val dateFormats = CustomJsonFormats.formats
val payloadToUpdate: Box[ConsentJWT] = JwtUtil.getSignedPayloadAsJson(consent.jsonWebToken) // Payload as JSON string
.map(net.liftweb.json.parse(_).extract[ConsentJWT]) // Extract case class

val availableAccountsUserIbans: List[String] = payloadToUpdate match {
case Full(consentJwt) =>
val availableAccountsUserIbans: List[String] =
if (consentJwt.access.map(_.availableAccounts.contains("allAccounts")).isDefined) {
// Get all accounts held by the current user
val userAccounts: List[BankIdAccountId] =
AccountHolders.accountHolders.vend.getAccountsHeldByUser(user, Some(null)).toList
userAccounts.flatMap { acc =>
BankAccountRouting.find(
By(BankAccountRouting.BankId, acc.bankId.value),
By(BankAccountRouting.AccountId, acc.accountId.value),
By(BankAccountRouting.AccountRoutingScheme, "IBAN")
).map(_.AccountRoutingAddress.get)
}
} else {
val emptyList: List[String] = Nil
emptyList
}
availableAccountsUserIbans
case _ =>
val emptyList: List[String] = Nil
emptyList
}


// 1. Add access
val availableAccounts: List[Future[ConsentView]] = availableAccountsUserIbans.distinct map { iban =>
Connector.connector.vend.getBankAccountByIban(iban, callContext) map { bankAccount =>
logger.debug(s"createBerlinGroupConsentJWT.accounts.bankAccount: $bankAccount")
val error = s"${InvalidConnectorResponse} IBAN: ${iban} ${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
)
}
}

Future.sequence(availableAccounts) map { views =>
if(views.isEmpty) {
Empty
} else {
val updatedPayload = payloadToUpdate.map(i =>
i.copy(views = views) // Update the field "views"
)
val jwtPayloadAsJson = compactRender(Extraction.decompose(updatedPayload))
val jwtClaims: JWTClaimsSet = JWTClaimsSet.parse(jwtPayloadAsJson)
val jwt = CertificateUtil.jwtWithHmacProtection(jwtClaims, consent.secret)
Consents.consentProvider.vend.setJsonWebToken(consent.consentId, jwt)
}
}
}

def updateUserIdOfBerlinGroupConsentJWT(createdByUserId: String,
consent: MappedConsent,
Expand Down
5 changes: 4 additions & 1 deletion obp-api/src/main/scala/code/api/util/ErrorMessages.scala
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,10 @@ object ErrorMessages {
s"OBP-20087: The current source view.can_revoke_access_to_custom_views is false."

val BerlinGroupConsentAccessIsEmpty = s"OBP-20088: An access must be requested."

val BerlinGroupConsentAccessRecurringIndicator = s"OBP-20089: Recurring indicator must be false when availableAccounts is used."
val BerlinGroupConsentAccessFrequencyPerDay = s"OBP-20090: Frequency per day must be 1 when availableAccounts is used."
val BerlinGroupConsentAccessAvailableAccounts = s"OBP-20091: availableAccounts must be exactly 'allAccounts'."

val UserNotSuperAdminOrMissRole = "OBP-20101: Current User is not super admin or is missing entitlements:"
val CannotGetOrCreateUser = "OBP-20102: Cannot get or create user."
val InvalidUserProvider = "OBP-20103: Invalid DAuth User Provider."
Expand Down
4 changes: 3 additions & 1 deletion obp-api/src/main/scala/code/consumer/ConsumerProvider.scala
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ trait ConsumersProvider {
developerEmail: Option[String],
redirectURL: Option[String],
createdByUserId: Option[String],
certificate: Option[String] = None): Box[Consumer]
certificate: Option[String] = None,
logoUrl: Option[String] = None
): Box[Consumer]
def populateMissingUUIDs(): Boolean

}
8 changes: 7 additions & 1 deletion obp-api/src/main/scala/code/model/OAuth.scala
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,9 @@ object MappedConsumersProvider extends ConsumersProvider with MdcLoggable {
developerEmail: Option[String],
redirectURL: Option[String],
createdByUserId: Option[String],
certificate: Option[String]): Box[Consumer] = {
certificate: Option[String],
logoUrl: Option[String],
): Box[Consumer] = {

val consumer: Box[Consumer] =
// 1st try to find via UUID issued by OBP-API back end
Expand Down Expand Up @@ -473,6 +475,10 @@ object MappedConsumersProvider extends ConsumersProvider with MdcLoggable {
case Some(v) => c.clientCertificate(v)
case None =>
}
logoUrl match {
case Some(v) => c.logoUrl(v)
case None =>
}
consumerId match {
case Some(v) => c.consumerId(v)
case None =>
Expand Down
27 changes: 24 additions & 3 deletions obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,19 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510
// Select all IBANs
selectedAccountsIbansValue.set(userIbans)

var canReadAccountsIbansAvailableAccounts: List[String] = List()
if(json.access.availableAccounts.contains("allAccounts")) { //
/*
Access is requested via:
"access":
{
"availableAccounts": "allAccounts"
}
*/
accessAccountsDefinedVar.set(true)
canReadAccountsIbansAvailableAccounts = userIbans.toList
}

// 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 => // Access is requested via "accounts": []
Expand Down Expand Up @@ -226,7 +239,7 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510
}

// all Selected IBANs
val ibansFromGetConsentResponseJson = (canReadAccountsIbans ::: canReadBalancesIbans ::: canReadTransactionsIbans).distinct
val ibansFromGetConsentResponseJson = (canReadAccountsIbansAvailableAccounts ::: canReadAccountsIbans ::: canReadBalancesIbans ::: canReadTransactionsIbans).distinct

/**
* Generates toggle switches for IBAN lists.
Expand Down Expand Up @@ -388,7 +401,9 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510
Consents.consentProvider.vend.getConsentByConsentId(consentId) match {
case Full(consent) if otpValue.is == consent.challenge =>
updateConsentUser(consent)
Consents.consentProvider.vend.updateConsentStatus(consentId, ConsentStatus.rejected)
updateConsentJwt(consent) map { i =>
Consents.consentProvider.vend.updateConsentStatus(consentId, ConsentStatus.rejected)
}
S.redirectTo(
s"$redirectUriValue?CONSENT_ID=${consentId}"
)
Expand All @@ -406,7 +421,9 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510
Consents.consentProvider.vend.getConsentByConsentId(consentId) match {
case Full(consent) if otpValue.is == consent.challenge =>
updateConsentUser(consent)
Consents.consentProvider.vend.updateConsentStatus(consentId, ConsentStatus.valid)
updateConsentJwt(consent) map { i =>
Consents.consentProvider.vend.updateConsentStatus(consentId, ConsentStatus.valid)
}
S.redirectTo(
s"/confirm-bg-consent-request-redirect-uri?CONSENT_ID=${consentId}"
)
Expand All @@ -421,6 +438,10 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510
val jwt = Consent.updateUserIdOfBerlinGroupConsentJWT(loggedInUser.userId, consent, None).openOrThrowException(ErrorMessages.InvalidConnectorResponse)
Consents.consentProvider.vend.setJsonWebToken(consent.consentId, jwt)
}
private def updateConsentJwt(consent: MappedConsent) = {
val loggedInUser = AuthUser.currentUser.flatMap(_.user.foreign).openOrThrowException(ErrorMessages.UserNotLoggedIn)
Consent.updateViewsOfBerlinGroupConsentJWT(loggedInUser, consent, None)
}

private def getTppRedirectUri() = {
val consentId = ObpS.param("CONSENT_ID") openOr ("")
Expand Down
Loading