diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template
index e2a7ddba7..385f196ea 100644
--- a/obp-api/src/main/resources/props/sample.props.template
+++ b/obp-api/src/main/resources/props/sample.props.template
@@ -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
diff --git a/obp-api/src/main/scala/code/api/cache/Redis.scala b/obp-api/src/main/scala/code/api/cache/Redis.scala
index 4c5412125..ed5d6856c 100644
--- a/obp-api/src/main/scala/code/api/cache/Redis.scala
+++ b/obp-api/src/main/scala/code/api/cache/Redis.scala
@@ -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)
diff --git a/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala b/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala
index 3cb253db0..c63fc89cc 100644
--- a/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala
+++ b/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala
@@ -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
}
diff --git a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala
new file mode 100644
index 000000000..cefb24abe
--- /dev/null
+++ b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala
@@ -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.")
+ }
+ }
+}
diff --git a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala
index 37061e2c2..e26c48520 100644
--- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala
+++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala
@@ -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
diff --git a/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala b/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala
index 9b8956ef9..59bcbfb57 100644
--- a/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala
+++ b/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala
@@ -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 .
-
-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 .
+ *
+ * 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
+
+
+
+ {iban}
+
+
+ } else {
+ // Show only the IBAN text when updateConsentPayloadValue is false
+
+ {iban}
+
+ }
+ }
+ }
+
+ // 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 $consumerName making the following actions on my behalf:
+ |""".stripMargin
+ // Converting formText into a NodeSeq for raw HTML
+ val formTextHtml: NodeSeq = scala.xml.XML.loadString("
" + formText + "
")
- "#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 *" #> (
+