mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 13:07:02 +00:00
Merge pull request #2495 from constantine2nd/develop
TPP cert, Consent page
This commit is contained in:
commit
7b41d15f56
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
153
obp-api/src/main/scala/code/api/util/CertificateVerifier.scala
Normal file
153
obp-api/src/main/scala/code/api/util/CertificateVerifier.scala
Normal 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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user