From f08dbfa6ede0bf7c66083d07da845215901af03c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Thu, 23 Jan 2025 13:33:33 +0100 Subject: [PATCH 01/12] feature/Add customer at Keycloak via restful api --- obp-api/pom.xml | 7 +- .../scala/code/api/util/KeycloakAdmin.scala | 94 +++++++++++++++++++ 2 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 obp-api/src/main/scala/code/api/util/KeycloakAdmin.scala diff --git a/obp-api/pom.xml b/obp-api/pom.xml index f5f88cc8ad..a7e6561184 100644 --- a/obp-api/pom.xml +++ b/obp-api/pom.xml @@ -490,7 +490,12 @@ com.squareup.okhttp3 okhttp - 4.9.1 + 4.12.0 + + + com.squareup.okhttp3 + logging-interceptor + 4.12.0 diff --git a/obp-api/src/main/scala/code/api/util/KeycloakAdmin.scala b/obp-api/src/main/scala/code/api/util/KeycloakAdmin.scala new file mode 100644 index 0000000000..600b9cdc06 --- /dev/null +++ b/obp-api/src/main/scala/code/api/util/KeycloakAdmin.scala @@ -0,0 +1,94 @@ +package code.api.util + + +import code.api.OAuth2Login.Keycloak +import net.liftweb.common.{Box, Failure, Full} +import okhttp3._ +import okhttp3.logging.HttpLoggingInterceptor +import org.slf4j.LoggerFactory + + +object KeycloakAdmin extends App { + + // Initialize Logback logger + private val logger = LoggerFactory.getLogger("okhttp3") + + // Define variables (replace with actual values) + private val keycloakHost = Keycloak.keycloakHost + private val realm = "master" + private val accessToken = "" + + def createHttpClientWithLogback(): OkHttpClient = { + val builder = new OkHttpClient.Builder() + val logging = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger { + override def log(message: String): Unit = logger.debug(message) + }) + logging.setLevel(HttpLoggingInterceptor.Level.BODY) // Log full request/response details + builder.addInterceptor(logging) + builder.build() + } + // Create OkHttp client with logging + val client = createHttpClientWithLogback() + + createClient( + "my-consumer-client", + "My Consume", + "Client for accessing API resources", + isPublic = false + ) + + def createClient(clientId: String, + name: String, + description: String, + isPublic: Boolean, + realm: String = realm + ): Unit = { + val url = s"$keycloakHost/admin/realms/$realm/clients" + // JSON request body + val jsonBody = + s"""{ + | "clientId": "$clientId", + | "name": "$name", + | "description": "$description", + | "enabled": true, + | "clientAuthenticatorType": "client-secret", + | "directAccessGrantsEnabled": true, + | "standardFlowEnabled": true, + | "implicitFlowEnabled": false, + | "serviceAccountsEnabled": true, + | "publicClient": false, + | "secret": "$isPublic" + |}""".stripMargin + + // Define the request with headers and JSON body + val requestBody = RequestBody.create(MediaType.get("application/json; charset=utf-8"), jsonBody) + + val request = new Request.Builder() + .url(url) + .post(requestBody) + .addHeader("Authorization", s"Bearer $accessToken") + .addHeader("Content-Type", "application/json") + .build() + + makeAndHandleHttpCall(request) + } + + private def makeAndHandleHttpCall(request: Request): Box[Boolean] = { + // Execute the request + try { + val response = client.newCall(request).execute() + if (response.isSuccessful) { + logger.debug(s"Response: ${response.body.string}") + Full(response.isSuccessful) + } else { + logger.error(s"Request failed with status code: ${response.code}") + logger.debug(s"Response: ${response}") + Failure(s"code: ${response.code} message: ${response.message}") + } + } catch { + case e: Exception => + logger.error(s"Error occurred: ${e.getMessage}") + Failure(e.getMessage) + } + } +} \ No newline at end of file From 54044cce691b6079c91b77d61558da356991f3bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Fri, 24 Jan 2025 16:46:40 +0100 Subject: [PATCH 02/12] feature/Add property integrate_with_keycloak --- .../resources/props/sample.props.template | 10 ++++- .../scala/code/api/util/KeycloakAdmin.scala | 39 ++++++++++++------- .../code/snippet/ConsumerRegistration.scala | 7 +++- 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 3b0478bb5f..d223b6e19f 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -764,14 +764,22 @@ display_internal_errors=false # URL of Public server JWK set used for validating bearer JWT access tokens # It can contain more than one URL i.e. list of uris. Values are comma separated. # oauth2.jwk_set.url=http://localhost:8080/jwk.json,https://www.googleapis.com/oauth2/v3/certs +# ------------------------------------------------------------------------------ OAuth 2 ------ + +# -- Keycloak OAuth 2 --------------------------------------------------------------------------- +# integrate_with_keycloak = false # Keycloak Identity Provider Host # oauth2.keycloak.host=http://localhost:7070 +# Keycloak access token to make a call to Admin APIs +# keycloak.admin.access_token = +# Keycloak Identity Provider Realm (Multi-Tenancy Support) +# oauth2.keycloak.realm = master # oauth2.keycloak.well_known=http://localhost:7070/realms/master/.well-known/openid-configuration # Used to sync IAM of OBP-API and IAM of Keycloak # oauth2.keycloak.source_of_truth = false # Resource access object allowed to sync IAM of OBP-API and IAM of Keycloak # oauth2.keycloak.resource_access_key_name_to_trust = open-bank-project -# ------------------------------------------------------------------------------ OAuth 2 ------ +# ------------------------------------------------------------------------ Keycloak OAuth 2 ------ # -- PSU Authentication methods -------------------------------------------------------------- # The EBA notes that there would appear to currently be three main ways or methods diff --git a/obp-api/src/main/scala/code/api/util/KeycloakAdmin.scala b/obp-api/src/main/scala/code/api/util/KeycloakAdmin.scala index 600b9cdc06..7ff765077f 100644 --- a/obp-api/src/main/scala/code/api/util/KeycloakAdmin.scala +++ b/obp-api/src/main/scala/code/api/util/KeycloakAdmin.scala @@ -2,21 +2,23 @@ package code.api.util import code.api.OAuth2Login.Keycloak +import code.model.{AppType, Consumer} import net.liftweb.common.{Box, Failure, Full} import okhttp3._ import okhttp3.logging.HttpLoggingInterceptor import org.slf4j.LoggerFactory -object KeycloakAdmin extends App { +object KeycloakAdmin { // Initialize Logback logger private val logger = LoggerFactory.getLogger("okhttp3") + val integrateWithKeycloak = APIUtil.getPropsAsBoolValue("integrate_with_keycloak", defaultValue = false) // Define variables (replace with actual values) private val keycloakHost = Keycloak.keycloakHost - private val realm = "master" - private val accessToken = "" + private val realm = APIUtil.getPropsValue(nameOfProperty = "oauth2.keycloak.realm", "master") + private val accessToken = APIUtil.getPropsValue(nameOfProperty = "keycloak.admin.access_token", "") def createHttpClientWithLogback(): OkHttpClient = { val builder = new OkHttpClient.Builder() @@ -30,19 +32,29 @@ object KeycloakAdmin extends App { // Create OkHttp client with logging val client = createHttpClientWithLogback() - createClient( - "my-consumer-client", - "My Consume", - "Client for accessing API resources", - isPublic = false - ) - + def createKeycloakConsumer(consumer: Consumer): Box[Boolean] = { + val isPublic = + AppType.valueOf(consumer.appType.get) match { + case AppType.Confidential => false + case _ => true + } + createClient( + clientId = consumer.key.get, + secret = consumer.secret.get, + name = consumer.name.get, + description = consumer.description.get, + redirectUri = consumer.redirectURL.get, + isPublic = isPublic, + ) + } def createClient(clientId: String, + secret: String, name: String, description: String, + redirectUri: String, isPublic: Boolean, realm: String = realm - ): Unit = { + ) = { val url = s"$keycloakHost/admin/realms/$realm/clients" // JSON request body val jsonBody = @@ -50,14 +62,15 @@ object KeycloakAdmin extends App { | "clientId": "$clientId", | "name": "$name", | "description": "$description", + | "redirectUris": ["$redirectUri"], | "enabled": true, | "clientAuthenticatorType": "client-secret", | "directAccessGrantsEnabled": true, | "standardFlowEnabled": true, | "implicitFlowEnabled": false, | "serviceAccountsEnabled": true, - | "publicClient": false, - | "secret": "$isPublic" + | "publicClient": $isPublic, + | "secret": "$secret" |}""".stripMargin // Define the request with headers and JSON body diff --git a/obp-api/src/main/scala/code/snippet/ConsumerRegistration.scala b/obp-api/src/main/scala/code/snippet/ConsumerRegistration.scala index 1203f2c4f3..0e25cf9236 100644 --- a/obp-api/src/main/scala/code/snippet/ConsumerRegistration.scala +++ b/obp-api/src/main/scala/code/snippet/ConsumerRegistration.scala @@ -27,9 +27,8 @@ TESOBE (http://www.tesobe.com/) package code.snippet import java.util - import code.api.{Constant, DirectLogin} -import code.api.util.{APIUtil, ErrorMessages, X509} +import code.api.util.{APIUtil, ErrorMessages, KeycloakAdmin, X509} import code.consumer.Consumers import code.model.dataAccess.AuthUser import code.model.{Consumer, _} @@ -176,6 +175,10 @@ class ConsumerRegistration extends MdcLoggable { oAuth2Client }) } + + // In case we use Keycloak as Identity Provider we create corresponding client at Keycloak side a well + if(KeycloakAdmin.integrateWithKeycloak) KeycloakAdmin.createKeycloakConsumer(consumer) + val registerConsumerSuccessMessageWebpage = getWebUiPropsValue( "webui_register_consumer_success_message_webpage", "Thanks for registering your consumer with the Open Bank Project API! Here is your developer information. Please save it in a secure location.") From 916db10889cca77c01f44cc73985850aa52fbd51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Mon, 27 Jan 2025 23:26:27 +0100 Subject: [PATCH 03/12] feature/Tweak VRP consent screen to show data via GUI instead of JSON --- .../code/snippet/VrpConsentCreation.scala | 33 +++- .../webapp/confirm-vrp-consent-request.html | 153 ++++++++++++++++-- 2 files changed, 173 insertions(+), 13 deletions(-) diff --git a/obp-api/src/main/scala/code/snippet/VrpConsentCreation.scala b/obp-api/src/main/scala/code/snippet/VrpConsentCreation.scala index b11c03b95d..a656c059bb 100644 --- a/obp-api/src/main/scala/code/snippet/VrpConsentCreation.scala +++ b/obp-api/src/main/scala/code/snippet/VrpConsentCreation.scala @@ -57,11 +57,38 @@ class VrpConsentCreation extends MdcLoggable with RestHelper with APIMethods510 case Right(response) => { tryo {json.parse(response).extract[ConsentRequestResponseJson]} match { case Full(consentRequestResponseJson) => + val jsonAst = consentRequestResponseJson.payload "#confirm-vrp-consent-request-form-title *" #> s"Please confirm or deny the following consent request:" & - "#confirm-vrp-consent-request-response-json *" #> s"""${json.prettyRender(json.Extraction.decompose(consentRequestResponseJson.payload))}""" & - "#confirm-vrp-consent-request-confirm-submit-button" #> SHtml.onSubmitUnit(confirmConsentRequestProcess) + // "#confirm-vrp-consent-request-response-json *" #> s"""${json.prettyRender(json.Extraction.decompose(consentRequestResponseJson.payload))}""" & + "#from_bank_routing_scheme [value]" #> s"${(jsonAst \ "from_account" \ "bank_routing" \ "scheme").extract[String]}" & + "#from_bank_routing_address [value]" #> s"${(jsonAst \ "from_account" \ "bank_routing" \ "address").extract[String]}" & + "#from_branch_routing_scheme [value]" #> s"${(jsonAst \ "from_account" \ "branch_routing" \ "scheme").extract[String]}" & + "#from_branch_routing_address [value]" #> s"${(jsonAst \ "from_account" \ "branch_routing" \ "address").extract[String]}" & + "#from_routing_scheme [value]" #> s"${(jsonAst \ "from_account" \ "account_routing" \ "scheme").extract[String]}" & + "#from_routing_address [value]" #> s"${(jsonAst \ "from_account" \ "account_routing" \ "address").extract[String]}" & + "#to_bank_routing_scheme [value]" #> s"${(jsonAst \ "to_account" \ "bank_routing" \ "scheme").extract[String]}" & + "#to_bank_routing_address [value]" #> s"${(jsonAst \ "to_account" \ "bank_routing" \ "address").extract[String]}" & + "#to_branch_routing_scheme [value]" #> s"${(jsonAst \ "to_account" \ "branch_routing" \ "scheme").extract[String]}" & + "#to_branch_routing_address [value]" #> s"${(jsonAst \ "to_account" \ "branch_routing" \ "address").extract[String]}" & + "#to_routing_scheme [value]" #> s"${(jsonAst \ "to_account" \ "account_routing" \ "scheme").extract[String]}" & + "#to_routing_address [value]" #> s"${(jsonAst \ "to_account" \ "account_routing" \ "address").extract[String]}" & + "#counterparty_name [value]" #> s"${(jsonAst \ "to_account" \ "counterparty_name").extract[String]}" & + "#currency [value]" #> s"${(jsonAst \ "to_account" \ "limit" \ "currency").extract[String]}" & + "#max_single_amount [value]" #> s"${(jsonAst \ "to_account" \ "limit" \ "max_single_amount").extract[String]}" & + "#max_monthly_amount [value]" #> s"${(jsonAst \ "to_account" \ "limit" \ "max_monthly_amount").extract[String]}" & + "#max_yearly_amount [value]" #> s"${(jsonAst \ "to_account" \ "limit" \ "max_yearly_amount").extract[String]}" & + "#max_total_amount [value]" #> s"${(jsonAst \ "to_account" \ "limit" \ "max_total_amount").extract[String]}" & + "#max_number_of_monthly_transactions [value]" #> s"${(jsonAst \ "to_account" \ "limit" \ "max_number_of_monthly_transactions").extract[String]}" & + "#max_number_of_yearly_transactions [value]" #> s"${(jsonAst \ "to_account" \ "limit" \ "max_number_of_yearly_transactions").extract[String]}" & + "#max_number_of_transactions [value]" #> s"${(jsonAst \ "to_account" \ "limit" \ "max_number_of_transactions").extract[String]}" & + "#time_to_live_in_seconds [value]" #> s"${(jsonAst \ "time_to_live").extract[String]}" & + "#valid_from [value]" #> s"${(jsonAst \ "valid_from").extract[String]}" & + "#email [value]" #> s"${(jsonAst \ "email").extract[String]}" & + "#phone_number [value]" #> s"${(jsonAst \ "phone_number").extract[String]}" & + "#confirm-vrp-consent-request-confirm-submit-button" #> SHtml.onSubmitUnit(confirmConsentRequestProcess) case _ => - "#confirm-vrp-consent-request-form-title *" #> s"Please confirm or deny the following consent request:" & + "#confirm-vrp-consent-request-form-title *" #> s"Please confirm or deny the following consent request:" & + "#confirm-vrp-consent-request-form-title *" #> s"Please confirm or deny the following consent request:" & "#confirm-vrp-consent-request-response-json *" #> s"""$InvalidJsonFormat The Json body should be the $ConsentRequestResponseJson. |Please check `Get Consent Request` endpoint separately! """.stripMargin & diff --git a/obp-api/src/main/webapp/confirm-vrp-consent-request.html b/obp-api/src/main/webapp/confirm-vrp-consent-request.html index f552251e8b..c1da8338ff 100644 --- a/obp-api/src/main/webapp/confirm-vrp-consent-request.html +++ b/obp-api/src/main/webapp/confirm-vrp-consent-request.html @@ -28,23 +28,156 @@ -->
- - From 993986f01fb3500419ce9952aabed9c769745d8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 28 Jan 2025 09:37:59 +0100 Subject: [PATCH 04/12] feature/Tweak VRP consent screen to show data via GUI instead of JSON 2 --- obp-api/src/main/webapp/confirm-vrp-consent-request.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/obp-api/src/main/webapp/confirm-vrp-consent-request.html b/obp-api/src/main/webapp/confirm-vrp-consent-request.html index c1da8338ff..b298e7d75a 100644 --- a/obp-api/src/main/webapp/confirm-vrp-consent-request.html +++ b/obp-api/src/main/webapp/confirm-vrp-consent-request.html @@ -179,5 +179,11 @@

Other

Deny

+ + From 8b63cf654ce9ced128e5088669a2bd35c38a735d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 28 Jan 2025 13:35:46 +0100 Subject: [PATCH 05/12] feature/Tweak VRP consent screen to show data via GUI instead of JSON 3 --- obp-api/src/main/webapp/confirm-vrp-consent-request.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obp-api/src/main/webapp/confirm-vrp-consent-request.html b/obp-api/src/main/webapp/confirm-vrp-consent-request.html index b298e7d75a..ffc18a4893 100644 --- a/obp-api/src/main/webapp/confirm-vrp-consent-request.html +++ b/obp-api/src/main/webapp/confirm-vrp-consent-request.html @@ -174,9 +174,9 @@

Other

+ Deny - Deny

+ + From 27b6bacca6946f4d877b49067abd6700f17baaec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Thu, 30 Jan 2025 17:00:15 +0100 Subject: [PATCH 08/12] feature/Berlin Group Consent acceptance --- .../main/scala/bootstrap/liftweb/Boot.scala | 2 + .../code/snippet/BerlinGroupConsent.scala | 114 ++++++++++++++++++ obp-api/src/main/scala/code/util/Helper.scala | 4 +- .../confirm-bg-consent-request-sca.html | 49 ++++++++ .../webapp/confirm-bg-consent-request.html | 57 +++++++++ obp-api/src/main/webapp/media/css/website.css | 3 + 6 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala create mode 100644 obp-api/src/main/webapp/confirm-bg-consent-request-sca.html create mode 100644 obp-api/src/main/webapp/confirm-bg-consent-request.html diff --git a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala index 88aa9229b8..4c708fe61f 100644 --- a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala @@ -594,6 +594,8 @@ class Boot extends MdcLoggable { Menu.i("Introduction") / "introduction", Menu.i("add-user-auth-context-update-request") / "add-user-auth-context-update-request", Menu.i("confirm-user-auth-context-update-request") / "confirm-user-auth-context-update-request", + Menu.i("confirm-bg-consent-request") / "confirm-bg-consent-request" >> AuthUser.loginFirst,//OAuth consent page, + Menu.i("confirm-bg-consent-request-sca") / "confirm-bg-consent-request-sca" >> AuthUser.loginFirst,//OAuth consent page, Menu.i("confirm-vrp-consent-request") / "confirm-vrp-consent-request" >> AuthUser.loginFirst,//OAuth consent page, Menu.i("confirm-vrp-consent") / "confirm-vrp-consent" >> AuthUser.loginFirst //OAuth consent page ) ++ accountCreation ++ Admin.menus diff --git a/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala b/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala new file mode 100644 index 0000000000..01fcf745e3 --- /dev/null +++ b/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala @@ -0,0 +1,114 @@ +/** +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.api.berlin.group.v1_3.JSONFactory_BERLIN_GROUP_1_3.{GetConsentResponseJson, createGetConsentResponseJson} +import code.api.util.CustomJsonFormats +import code.api.v3_1_0.APIMethods310 +import code.api.v5_0_0.APIMethods500 +import code.api.v5_1_0.APIMethods510 +import code.consent.{Consents, MappedConsent} +import code.consumer.Consumers +import code.model.dataAccess.AuthUser +import code.util.Helper.{MdcLoggable, ObpS} +import net.liftweb.common.{Box, Failure, Full} +import net.liftweb.http.rest.RestHelper +import net.liftweb.http.{RequestVar, S, SHtml, SessionVar} +import net.liftweb.json.Formats +import net.liftweb.util.CssSel +import net.liftweb.util.Helpers._ + +class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 with APIMethods500 with APIMethods310 { + protected implicit override def formats: Formats = CustomJsonFormats.formats + + private object otpValue extends RequestVar("123456") + private object redirectUriValue extends SessionVar("") + + def confirmBerlinGroupConsentRequest: CssSel = { + callGetConsentByConsentId() match { + case Full(consent) => + val json: GetConsentResponseJson = createGetConsentResponseJson(consent) + val consumer = Consumers.consumers.vend.getConsumerByConsumerId(consent.consumerId) + val uri: String = consumer.map(_.redirectURL.get).getOrElse("none") + redirectUriValue.set(uri) + 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 + + + "#confirm-bg-consent-request-form-title *" #> s"Please confirm or deny the following consent request:" & + "#confirm-bg-consent-request-form-text *" #> s"""$formText""" & + "#confirm-bg-consent-request-confirm-submit-button" #> SHtml.onSubmitUnit(confirmConsentRequestProcess) + case everythingElse => + S.error(everythingElse.toString) + "#confirm-bg-consent-request-form-title *" #> s"Please confirm or deny the following consent request:" & + "type=submit" #> "" + } + } + + private def callGetConsentByConsentId(): Box[MappedConsent] = { + val requestParam = List( + ObpS.param("CONSENT_ID"), + ) + if (requestParam.count(_.isDefined) < requestParam.size) { + Failure("Parameter CONSENT_ID is missing, please set it in the URL") + } else { + val consentId = ObpS.param("CONSENT_ID") openOr ("") + Consents.consentProvider.vend.getConsentByConsentId(consentId) + } + } + + private def confirmConsentRequestProcess() = { + val consentId = ObpS.param("CONSENT_ID") openOr ("") + S.redirectTo( + s"/confirm-bg-consent-request-sca?CONSENT_ID=${consentId}" + ) + } + private def confirmConsentRequestProcessSca() = { + val consentId = ObpS.param("CONSENT_ID") openOr ("") + S.redirectTo( + s"$redirectUriValue?CONSENT_ID=${consentId}" + ) + } + + + def confirmBgConsentRequest = { + "#otp-value" #> SHtml.textElem(otpValue) & + "type=submit" #> SHtml.onSubmitUnit(confirmConsentRequestProcessSca) + } + +} diff --git a/obp-api/src/main/scala/code/util/Helper.scala b/obp-api/src/main/scala/code/util/Helper.scala index 74e44e83e3..e8592073f5 100644 --- a/obp-api/src/main/scala/code/util/Helper.scala +++ b/obp-api/src/main/scala/code/util/Helper.scala @@ -217,7 +217,9 @@ object Helper extends Loggable { "/dummy-user-tokens","/create-sandbox-account", "/add-user-auth-context-update-request","/otp", "/terms-and-conditions", "/privacy-policy", - "/confirm-vrp-consent-request", + "/confirm-bg-consent-request", + "/confirm-bg-consent-request-sca", + "/confirm-vrp-consent-request", "/confirm-vrp-consent", "/consent-screen", "/consent", diff --git a/obp-api/src/main/webapp/confirm-bg-consent-request-sca.html b/obp-api/src/main/webapp/confirm-bg-consent-request-sca.html new file mode 100644 index 0000000000..0aa7fdae4c --- /dev/null +++ b/obp-api/src/main/webapp/confirm-bg-consent-request-sca.html @@ -0,0 +1,49 @@ + + +
+ +
+ diff --git a/obp-api/src/main/webapp/confirm-bg-consent-request.html b/obp-api/src/main/webapp/confirm-bg-consent-request.html new file mode 100644 index 0000000000..d3e2ffcff1 --- /dev/null +++ b/obp-api/src/main/webapp/confirm-bg-consent-request.html @@ -0,0 +1,57 @@ + + +
+ +
diff --git a/obp-api/src/main/webapp/media/css/website.css b/obp-api/src/main/webapp/media/css/website.css index b8742d5279..2921646d8f 100644 --- a/obp-api/src/main/webapp/media/css/website.css +++ b/obp-api/src/main/webapp/media/css/website.css @@ -408,6 +408,7 @@ input{ } #add-user-auth-context-update-request-div form, +#confirm-bg-consent-request-sca form, #confirm-user-auth-context-update-request-div form{ max-width: 500px; margin: 0 auto; @@ -415,6 +416,7 @@ input{ } #add-user-auth-context-update-request-div #identifier-error-div, +#confirm-bg-consent-request-sca #identifier-error-div, #confirm-user-auth-context-update-request-div #otp-value-error-div{ text-align: justify; color: black; @@ -430,6 +432,7 @@ input{ } #add-user-auth-context-update-request-div #identifier-error .error, +#confirm-bg-consent-request-sca #identifier-error .error, #confirm-user-auth-context-update-request-div #otp-value-error .error{ background-color: white; font-family: Roboto-Regular,sans-serif; From a96bc08f912ac62aa1cf4c612e375fe0c4071845 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Thu, 30 Jan 2025 20:05:48 +0100 Subject: [PATCH 09/12] feature/Tweak VRP Consent acceptance --- .../code/snippet/VrpConsentCreation.scala | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/obp-api/src/main/scala/code/snippet/VrpConsentCreation.scala b/obp-api/src/main/scala/code/snippet/VrpConsentCreation.scala index d48bc14fbd..97e4ad58b3 100644 --- a/obp-api/src/main/scala/code/snippet/VrpConsentCreation.scala +++ b/obp-api/src/main/scala/code/snippet/VrpConsentCreation.scala @@ -33,11 +33,12 @@ import code.api.v5_1_0.{APIMethods510, ConsentJsonV510} import code.api.v5_0_0.{APIMethods500, ConsentJsonV500, ConsentRequestResponseJson} import code.api.v3_1_0.{APIMethods310, ConsentChallengeJsonV310, ConsumerJsonV310} import code.consent.ConsentStatus +import code.consumer.Consumers import code.model.dataAccess.AuthUser import code.util.Helper.{MdcLoggable, ObpS} import net.liftweb.common.Full import net.liftweb.http.rest.RestHelper -import net.liftweb.http.{GetRequest, PostRequest, RequestVar, S, SHtml} +import net.liftweb.http.{GetRequest, PostRequest, RequestVar, S, SHtml, SessionVar} import net.liftweb.json import net.liftweb.json.Formats import net.liftweb.util.CssSel @@ -47,7 +48,8 @@ class VrpConsentCreation extends MdcLoggable with RestHelper with APIMethods510 protected implicit override def formats: Formats = CustomJsonFormats.formats private object otpValue extends RequestVar("123456") - + private object consentRequestIdValue extends SessionVar("") + def confirmVrpConsentRequest = { getConsentRequest match { case Left(error) => { @@ -62,8 +64,9 @@ class VrpConsentCreation extends MdcLoggable with RestHelper with APIMethods510 val jsonAst = consentRequestResponseJson.payload val currency = (jsonAst \ "to_account" \ "limit" \ "currency").extract[String] val ttl: Long = (jsonAst \ "time_to_live").extract[Long] + val consumer = Consumers.consumers.vend.getConsumerByConsumerId(consentRequestResponseJson.consumer_id) val formText = - s"""I, ${AuthUser.currentUser.map(_.firstName.get).getOrElse("")} ${AuthUser.currentUser.map(_.lastName.get).getOrElse("")}, consent to the service provider making transfers on my behalf from my bank account number ${(jsonAst \ "from_account" \ "account_routing" \ "address").extract[String]}, to the beneficiary ${(jsonAst \ "to_account" \ "counterparty_name").extract[String]}, account number ${(jsonAst \ "to_account" \ "account_routing" \ "address").extract[String]} at bank code ${(jsonAst \ "to_account" \ "bank_routing" \ "address").extract[String]}. + 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 transfers on my behalf from my bank account number ${(jsonAst \ "from_account" \ "account_routing" \ "address").extract[String]}, to the beneficiary ${(jsonAst \ "to_account" \ "counterparty_name").extract[String]}, account number ${(jsonAst \ "to_account" \ "account_routing" \ "address").extract[String]} at bank code ${(jsonAst \ "to_account" \ "bank_routing" \ "address").extract[String]}. | |The transfers governed by this consent must respect the following rules: | @@ -285,7 +288,7 @@ class VrpConsentCreation extends MdcLoggable with RestHelper with APIMethods510 } private def getConsentRequest: Either[(String, Int), String] = { - + val requestParam = List( ObpS.param("CONSENT_REQUEST_ID"), ) @@ -293,11 +296,14 @@ class VrpConsentCreation extends MdcLoggable with RestHelper with APIMethods510 if(requestParam.count(_.isDefined) < requestParam.size) { return Left(("Parameter CONSENT_REQUEST_ID is missing, please set it in the URL", 500)) } - + + val consentRequestId = ObpS.param("CONSENT_REQUEST_ID")openOr("") + consentRequestIdValue.set(consentRequestId) + val pathOfEndpoint = List( "consumer", "consent-requests", - ObpS.param("CONSENT_REQUEST_ID")openOr("") + consentRequestId ) val authorisationsResult = callEndpoint(Implementations5_0_0.getConsentRequest, pathOfEndpoint, GetRequest) From 532988f5a521fd691754ef07a469cf07a92b179f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Fri, 31 Jan 2025 11:43:23 +0100 Subject: [PATCH 10/12] feature/Improve Berlin Group Consent acceptance --- .../code/snippet/BerlinGroupConsent.scala | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala b/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala index 01fcf745e3..30e314df3d 100644 --- a/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala +++ b/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala @@ -26,22 +26,25 @@ TESOBE (http://www.tesobe.com/) */ package code.snippet +import code.api.RequestHeader import code.api.berlin.group.v1_3.JSONFactory_BERLIN_GROUP_1_3.{GetConsentResponseJson, createGetConsentResponseJson} -import code.api.util.CustomJsonFormats +import code.api.util.{ConsentJWT, CustomJsonFormats, JwtUtil} import code.api.v3_1_0.APIMethods310 import code.api.v5_0_0.APIMethods500 import code.api.v5_1_0.APIMethods510 -import code.consent.{Consents, MappedConsent} +import code.consent.{ConsentStatus, Consents, MappedConsent} import code.consumer.Consumers import code.model.dataAccess.AuthUser import code.util.Helper.{MdcLoggable, ObpS} import net.liftweb.common.{Box, Failure, Full} import net.liftweb.http.rest.RestHelper import net.liftweb.http.{RequestVar, S, SHtml, SessionVar} -import net.liftweb.json.Formats +import net.liftweb.json.{Formats, parse} import net.liftweb.util.CssSel import net.liftweb.util.Helpers._ +import scala.collection.immutable + class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 with APIMethods500 with APIMethods310 { protected implicit override def formats: Formats = CustomJsonFormats.formats @@ -53,7 +56,13 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 case Full(consent) => val json: GetConsentResponseJson = createGetConsentResponseJson(consent) val consumer = Consumers.consumers.vend.getConsumerByConsumerId(consent.consumerId) - val uri: String = consumer.map(_.redirectURL.get).getOrElse("none") + val consentJwt: Box[ConsentJWT] = JwtUtil.getSignedPayloadAsJson(consent.jsonWebToken).map(parse(_) + .extract[ConsentJWT]) + 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) 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. @@ -73,6 +82,7 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 "#confirm-bg-consent-request-form-title *" #> s"Please confirm or deny the following consent request:" & "#confirm-bg-consent-request-form-text *" #> s"""$formText""" & "#confirm-bg-consent-request-confirm-submit-button" #> SHtml.onSubmitUnit(confirmConsentRequestProcess) + "#confirm-bg-consent-request-deny-submit-button" #> SHtml.onSubmitUnit(denyConsentRequestProcess) case everythingElse => S.error(everythingElse.toString) "#confirm-bg-consent-request-form-title *" #> s"Please confirm or deny the following consent request:" & @@ -98,15 +108,23 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 s"/confirm-bg-consent-request-sca?CONSENT_ID=${consentId}" ) } + private def denyConsentRequestProcess() = { + val consentId = ObpS.param("CONSENT_ID") openOr ("") + Consents.consentProvider.vend.updateConsentStatus(consentId, ConsentStatus.rejected) + S.redirectTo( + s"$redirectUriValue?CONSENT_ID=${consentId}" + ) + } private def confirmConsentRequestProcessSca() = { val consentId = ObpS.param("CONSENT_ID") openOr ("") + Consents.consentProvider.vend.updateConsentStatus(consentId, ConsentStatus.valid) S.redirectTo( s"$redirectUriValue?CONSENT_ID=${consentId}" ) } - def confirmBgConsentRequest = { + def confirmBgConsentRequest: CssSel = { "#otp-value" #> SHtml.textElem(otpValue) & "type=submit" #> SHtml.onSubmitUnit(confirmConsentRequestProcessSca) } From c5c239981e5501f60f6966143d76037d5693036a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Fri, 31 Jan 2025 14:23:02 +0100 Subject: [PATCH 11/12] docfix/Tweak props keycloak.admin.access_token --- obp-api/src/main/resources/props/sample.props.template | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 23f612b98c..292a73b249 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -770,7 +770,7 @@ display_internal_errors=false # integrate_with_keycloak = false # Keycloak Identity Provider Host # oauth2.keycloak.host=http://localhost:7070 -# Keycloak access token to make a call to Admin APIs +# Keycloak access token to make a call to Admin APIs (This props is likely to change) # keycloak.admin.access_token = # Keycloak Identity Provider Realm (Multi-Tenancy Support) # oauth2.keycloak.realm = master From b1769970191661a53edf66f66c6612ea6796bc29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Fri, 31 Jan 2025 14:28:05 +0100 Subject: [PATCH 12/12] feature/Tweak VRP Consent format query param --- obp-api/src/main/scala/code/snippet/VrpConsentCreation.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/obp-api/src/main/scala/code/snippet/VrpConsentCreation.scala b/obp-api/src/main/scala/code/snippet/VrpConsentCreation.scala index 97e4ad58b3..d4a2102bfd 100644 --- a/obp-api/src/main/scala/code/snippet/VrpConsentCreation.scala +++ b/obp-api/src/main/scala/code/snippet/VrpConsentCreation.scala @@ -126,7 +126,7 @@ class VrpConsentCreation extends MdcLoggable with RestHelper with APIMethods510 def showHideElements: CssSel = { if (ObpS.param("format").isEmpty) { "#confirm-vrp-consent-request-form-text-div [style]" #> "display:block" & - "#confirm-vrp-consent-request-form-fields [style]" #> "display:block" + "#confirm-vrp-consent-request-form-fields [style]" #> "display:none" } else if(ObpS.param("format").contains("1")) { "#confirm-vrp-consent-request-form-text-div [style]" #> "display:none" & "#confirm-vrp-consent-request-form-fields [style]" #> "display:block" @@ -138,7 +138,7 @@ class VrpConsentCreation extends MdcLoggable with RestHelper with APIMethods510 "#confirm-vrp-consent-request-form-fields [style]" #> "display:block" } else { "#confirm-vrp-consent-request-form-text-div [style]" #> "display:block" & - "#confirm-vrp-consent-request-form-fields [style]" #> "display:block" + "#confirm-vrp-consent-request-form-fields [style]" #> "display:none" } }