Merge pull request #2495 from constantine2nd/develop

TPP cert, Consent page
This commit is contained in:
Simon Redfern 2025-02-26 14:15:42 +01:00 committed by GitHub
commit 7b41d15f56
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 465 additions and 55 deletions

View File

@ -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

View File

@ -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)

View File

@ -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
}

View File

@ -0,0 +1,153 @@
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 extends MdcLoggable {
/**
* 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.
*
* @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")
.or(APIUtil.getPropsValue("truststore.path")).getOrElse("")
val trustStorePassword = APIUtil.getPropsValue("truststore.password.tpp_signature", "").toCharArray
Try {
val trustStore = KeyStore.getInstance("PKCS12")
val trustStoreInputStream = new FileInputStream(trustStorePath)
try {
trustStore.load(trustStoreInputStream, trustStorePassword)
} finally {
trustStoreInputStream.close()
}
trustStore
} match {
case Success(store) =>
logger.info(s"Loaded trust store from: $trustStorePath")
Some(store)
case Failure(e) =>
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 validateCertificate(pemCertificate: String): Boolean = {
Try {
val certificate = parsePemToX509Certificate(pemCertificate)
// Load trust store
val trustStore = loadTrustStore()
.getOrElse(throw new Exception("Trust store could not be loaded."))
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm)
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)
logger.info("Certificate is valid and trusted.")
true
} match {
case Success(_) => true
case Failure(e: CertPathValidatorException) =>
logger.info(s"Certificate validation failed: ${e.getMessage}")
false
case Failure(e) =>
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-----", "")
.replaceAll("\\s", "")
val decoded = Base64.getDecoder.decode(cleanedPem)
val certFactory = CertificateFactory.getInstance("X.509")
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) =>
logger.error(s"Failed to load PEM certificate from file: ${exception.getMessage}")
None
}
}
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)
pemCertificate.foreach { pem =>
val isValid = validateCertificate(pem)
logger.info(s"Certificate verification result: $isValid")
}
loadTrustStore().foreach { trustStore =>
logger.info(s"Trust Store contains ${trustStore.size()} certificates.")
}
}
}

View File

@ -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

View File

@ -1,89 +1,302 @@
/**
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 <http://www.gnu.org/licenses/>.
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 <http://www.gnu.org/licenses/>.
*
* 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
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
/**
* 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("")
// 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
// 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(", ")}")
super.set(value)
}
}
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.
*
* @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))
val transactionsList = transactions.map(iban => ConsentAccessAccountsJson(iban = Some(iban), None, None, None, None, None))
ConsentAccessJson(
accounts = Some(accountsList), // Populate accounts
balances = if (balancesList.nonEmpty) Some(balancesList) else None, // Populate balances
transactions = if (transactionsList.nonEmpty) Some(transactionsList) else None // Populate transactions
)
}
/**
* Updates the consent with new IBANs for accounts, balances, and transactions.
*
* @param consentId The ID of the consent to update.
* @param ibans List of IBANs for accounts.
* @return Future[MappedConsent] representing the updated consent.
*/
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 {
APIUtil.unboxFullOrFail(_, None, s"$ConsentNotFound ($consentId)", 404)
}
// Update the consent JWT with new access details
consentJWT <- Consent.updateAccountAccessOfBerlinGroupConsentJWT(
createConsentAccessJson(
ibans,
if(canReadBalances) ibans else List(),
if(canReadTransactions) ibans else List()
),
consent,
None
) 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)
}
} yield {
updatedConsent
}
}
/**
* 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 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)
}
// 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 => // Access is requested
updateConsentPayloadValue.set(true)
accessAccountsDefinedVar.set(true)
userIbans.toList
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) => // Logged in user is not an owner of IBAN/IBANs
userIsOwnerOfAccountsValue.set(false)
accessAccountsDefinedVar.set(true)
accounts.flatMap(_.iban)
case None => // Access is not requested
accessAccountsDefinedVar.set(false)
List()
}
val canReadBalancesIbans: List[String] = json.access.balances match {
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) => // Access is requested for specific IBANs
accessBalancesDefinedVar.set(true)
balances.flatMap(_.iban)
case Some(balances) => // Logged in user is not an owner of IBAN/IBANs
userIsOwnerOfAccountsValue.set(false)
accessBalancesDefinedVar.set(true)
balances.flatMap(_.iban)
case None => // Access is not requested
accessBalancesDefinedVar.set(false)
List()
}
val canReadTransactionsIbans: List[String] = json.access.transactions match {
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) => // Access is requested for specific IBANs
accessTransactionsDefinedVar.set(true)
transactions.flatMap(_.iban)
case Some(transactions) => // Logged in user is not an owner of IBAN/IBANs
userIsOwnerOfAccountsValue.set(false)
accessTransactionsDefinedVar.set(true)
transactions.flatMap(_.iban)
case None => // Access is not requested
accessTransactionsDefinedVar.set(false)
List()
}
/**
* 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) {
// Show toggle switch when updateConsentPayloadValue is true
<div class="toggle-container">
<label class="switch">
{SHtml.ajaxCheckbox(selectedList.contains(iban), checked => {
if (checked) {
sessionVar.set(selectedList + iban) // Add to selected
} else {
sessionVar.set(selectedList - iban) // Remove from selected
}
JsCmds.Noop // Prevents page reload
}, "id" -> (iban + scope), "class" -> "toggle-input")}<span class="slider round"></span>
</label>
<span style="all: unset;" class="toggle-label">
{iban}
</span>
</div>
} else {
// Show only the IBAN text when updateConsentPayloadValue is false
<span style="all: unset;" class="toggle-label">
{iban}
</span>
}
}
}
// 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 <strong>$consumerName</strong> making the following actions on my behalf:
|""".stripMargin
// Converting formText into a NodeSeq for raw HTML
val formTextHtml: NodeSeq = scala.xml.XML.loadString("<div>" + formText + "</div>")
"#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 *" #> (
<div>
<p>
{formTextHtml}
</p>
<div>
<p><strong>Allowed actions:</strong></p>
<p style="padding-left: 20px">Read account details</p>
<p style={if (accessBalancesDefinedVar.is) "padding-left: 20px;" else "padding-left: 20px; display: none;"}>Read account balances</p>
<p style={if (accessTransactionsDefinedVar.is) "padding-left: 20px;" else "padding-left: 20px; display: none;"}>Read transactions</p>
</div>
<div>
<p><strong>Accounts</strong>:</p>
<div style="padding-left: 20px">
{generateCheckboxes("canReadAccountsIbans", userIbans.toList, selectedAccountsIbansValue.is, selectedAccountsIbansValue)}
</div>
<br/>
</div>
<p>This consent will end on date {json.validUntil}.</p>
<p>I understand that I can revoke this consent at any time.</p>
</div>
) & {
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:" &
@ -91,6 +304,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"),
@ -103,12 +321,31 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510
}
}
/**
* Handles the confirmation of a consent request.
*/
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) {
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,
accessBalancesDefinedVar.is,
accessTransactionsDefinedVar.is
)
}
S.redirectTo(
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)
@ -116,6 +353,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 {
@ -129,7 +370,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)

View File

@ -32,7 +32,7 @@ Berlin 13359, Germany
<div class="form-group">
<h3 id="confirm-bg-consent-request-form-title">Please check the Berlin Group Consent Request: </h3>
<div id="confirm-bg-consent-request-form-text-div">
<pre id="confirm-bg-consent-request-form-text"></pre>
<div id="confirm-bg-consent-request-form-text"></div>
</div>
</div>
<form method="post">