From 36b48a57ceabcb5536a237e31adf8b9a990ed4b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 4 Dec 2024 11:26:54 +0100 Subject: [PATCH 1/5] feature/Add PSU Authentication methods props psu_authentication_method --- .../resources/props/sample.props.template | 22 +++++++ .../AccountInformationServiceAISApi.scala | 2 +- .../v1_3/JSONFactory_BERLIN_GROUP_1_3.scala | 65 +++++++++++++++++-- 3 files changed, 81 insertions(+), 8 deletions(-) diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 8f287be25b..d2ca907cf1 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -771,6 +771,28 @@ display_internal_errors=false # oauth2.keycloak.source-of-truth = false # ------------------------------------------------------------------------------ OAuth 2 ------ +# -- PSU Authentication methods -------------------------------------------------------------- +# The EBA notes that there would appear to currently be three main ways or methods +# of carrying out the authentication procedure of the PSU through a dedicated interface, +# and APIs in particular, namely: +# - redirection, +# - embedded approaches and +# - decoupled approaches (or a combination thereof). +# In the cases of redirection and decoupled approaches, +# PSU’s authentication data is exchanged directly between PSUs and ASPSPs, +# as opposed to embedded approaches, in which PSU’s authentication data +# is exchanged between TPPs and ASPSPs through the interface. +#### +# psu_authentication_method = redirection_with_dedicated_start_of_authorization +# Possible values: +# - redirection +# - redirection_with_dedicated_start_of_authorization +# - embedded +# - decoupled +# In case that "psu_authentication_method = redirection" you must define +# psu_authentication_method_sca_redirect_url = redirect_url_value +# -------------------------------------------------------------- Authentication methods -- + ## This property is used for documenting at Resource Doc. It may include the port also (but not /obp) ## (this needs to be a URL) documented_server_url=https://apisandbox.openbankproject.com diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApi.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApi.scala index c8fbc2dd42..55adae9011 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApi.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/AccountInformationServiceAISApi.scala @@ -130,7 +130,7 @@ As a last option, an ASPSP might in addition accept a command with access rights PostConsentResponseJson( consentId = "1234-wertiq-983", consentStatus = "received", - _links = ConsentLinksV13("/v1.3/consents/1234-wertiq-983/authorisations") + _links = ConsentLinksV13(Some(Href("/v1.3/consents/1234-wertiq-983/authorisations"))) ), List(UserNotLoggedIn, UnknownError), ApiTag("Account Information Service (AIS)") :: apiTagBerlinGroupM :: Nil diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala index 7e9c3240dc..cdac221e5a 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala @@ -2,9 +2,9 @@ package code.api.berlin.group.v1_3 import java.text.SimpleDateFormat import java.util.Date - import code.api.berlin.group.v1_3.model._ import code.api.util.APIUtil._ +import code.api.util.ErrorMessages.MissingPropsValueAtThisInstance import code.api.util.{APIUtil, ConsentJWT, CustomJsonFormats, JwtUtil} import code.bankconnectors.Connector import code.consent.ConsentTrait @@ -15,6 +15,7 @@ import net.liftweb.common.Box.tryo import net.liftweb.common.{Box, Full} import net.liftweb.json import net.liftweb.json.{JValue, parse} + import scala.collection.immutable.List case class JvalueCaseClass(jvalueToCaseclass: JValue) @@ -231,7 +232,12 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats { combinedServiceIndicator: Boolean ) case class ConsentLinksV13( - startAuthorisation: String + startAuthorisation: Option[Href] = None, + scaRedirect: Option[Href] = None, + status: Option[Href] = None, + scaStatus: Option[Href] = None, + startAuthorisationWithPsuIdentification: Option[Href] = None, + startAuthorisationWithPsuAuthentication: Option[Href] = None, ) case class PostConsentResponseJson( @@ -239,6 +245,7 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats { consentStatus: String, _links: ConsentLinksV13 ) + case class Href(href: String) case class PutConsentResponseJson( scaStatus: String, @@ -246,6 +253,8 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats { ) + + case class GetConsentResponseJson( access: ConsentAccessJson, recurringIndicator: Boolean, @@ -508,11 +517,53 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats { } def createPostConsentResponseJson(consent: ConsentTrait) : PostConsentResponseJson = { - PostConsentResponseJson( - consentId = consent.consentId, - consentStatus = consent.status.toLowerCase(), - _links= ConsentLinksV13(s"/v1.3/consents/${consent.consentId}/authorisations") - ) + getPropsValue("psu_authentication_method") match { + case Full("redirection") => + val scaRedirectUrl = getPropsValue("psu_authentication_method_sca_redirect_url") + .openOr(MissingPropsValueAtThisInstance + "psu_authentication_method_sca_redirect_url") + PostConsentResponseJson( + consentId = consent.consentId, + consentStatus = consent.status.toLowerCase(), + _links = ConsentLinksV13( + scaRedirect = Some(Href(s"$scaRedirectUrl")), + status = Some(Href(s"/v1.3/consents/${consent.consentId}/status")), + scaStatus = Some(Href(s"/v1.3/consents/${consent.consentId}/authorisations/AUTHORISATIONID")), + ) + ) + case Full("redirection_with_dedicated_start_of_authorization") => + PostConsentResponseJson( + consentId = consent.consentId, + consentStatus = consent.status.toLowerCase(), + _links = ConsentLinksV13( + startAuthorisation = Some(Href(s"/v1.3/consents/${consent.consentId}/authorisations")) + ) + ) + case Full("embedded") => + PostConsentResponseJson( + consentId = consent.consentId, + consentStatus = consent.status.toLowerCase(), + _links = ConsentLinksV13( + startAuthorisationWithPsuAuthentication = Some(Href(s"/v1.3/consents/${consent.consentId}/authorisations")) + ) + ) + case Full("decoupled") => + PostConsentResponseJson( + consentId = consent.consentId, + consentStatus = consent.status.toLowerCase(), + _links = ConsentLinksV13( + startAuthorisationWithPsuIdentification = Some(Href(s"/v1.3/consents/${consent.consentId}/authorisations")) + ) + ) + case _ => + PostConsentResponseJson( + consentId = consent.consentId, + consentStatus = consent.status.toLowerCase(), + _links = ConsentLinksV13( + startAuthorisation = Some(Href(s"/v1.3/consents/${consent.consentId}/authorisations")) + ) + ) + } + } def createPutConsentResponseJson(consent: ConsentTrait) : ScaStatusResponse = { ScaStatusResponse( From da07b2e4de1c0cfe1cb1b6063006b5d73d5a7203 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 4 Dec 2024 15:42:14 +0100 Subject: [PATCH 2/5] feature/Tweak BG Create Consent Response --- .../api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala index cdac221e5a..4439b781ab 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala @@ -525,7 +525,7 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats { consentId = consent.consentId, consentStatus = consent.status.toLowerCase(), _links = ConsentLinksV13( - scaRedirect = Some(Href(s"$scaRedirectUrl")), + scaRedirect = Some(Href(s"$scaRedirectUrl/${consent.consentId}")), status = Some(Href(s"/v1.3/consents/${consent.consentId}/status")), scaStatus = Some(Href(s"/v1.3/consents/${consent.consentId}/authorisations/AUTHORISATIONID")), ) From 1c740b5b8d678cd998265bfc8f406765ca6416cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Fri, 6 Dec 2024 17:13:51 +0100 Subject: [PATCH 3/5] feature/Tweak BG Create Consent Response 2 --- .../v1_3/JSONFactory_BERLIN_GROUP_1_3.scala | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala b/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala index 4439b781ab..b1c2ea3290 100644 --- a/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala +++ b/obp-api/src/main/scala/code/api/berlin/group/v1_3/JSONFactory_BERLIN_GROUP_1_3.scala @@ -517,6 +517,16 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats { } def createPostConsentResponseJson(consent: ConsentTrait) : PostConsentResponseJson = { + def redirectionWithDedicatedStartOfAuthorization = { + PostConsentResponseJson( + consentId = consent.consentId, + consentStatus = consent.status.toLowerCase(), + _links = ConsentLinksV13( + startAuthorisation = Some(Href(s"/v1.3/consents/${consent.consentId}/authorisations")) + ) + ) + } + getPropsValue("psu_authentication_method") match { case Full("redirection") => val scaRedirectUrl = getPropsValue("psu_authentication_method_sca_redirect_url") @@ -531,13 +541,7 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats { ) ) case Full("redirection_with_dedicated_start_of_authorization") => - PostConsentResponseJson( - consentId = consent.consentId, - consentStatus = consent.status.toLowerCase(), - _links = ConsentLinksV13( - startAuthorisation = Some(Href(s"/v1.3/consents/${consent.consentId}/authorisations")) - ) - ) + redirectionWithDedicatedStartOfAuthorization case Full("embedded") => PostConsentResponseJson( consentId = consent.consentId, @@ -555,13 +559,7 @@ object JSONFactory_BERLIN_GROUP_1_3 extends CustomJsonFormats { ) ) case _ => - PostConsentResponseJson( - consentId = consent.consentId, - consentStatus = consent.status.toLowerCase(), - _links = ConsentLinksV13( - startAuthorisation = Some(Href(s"/v1.3/consents/${consent.consentId}/authorisations")) - ) - ) + redirectionWithDedicatedStartOfAuthorization } } From ce73ee5ad606cccebcfaed1a7016de93325ecb77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Mon, 9 Dec 2024 18:32:43 +0100 Subject: [PATCH 4/5] feature/Add consent_id query parameter --- obp-api/src/main/scala/code/api/util/APIUtil.scala | 7 +++++-- obp-api/src/main/scala/code/api/util/OBPParam.scala | 1 + obp-api/src/main/scala/code/consent/MappedConsent.scala | 6 ++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/APIUtil.scala b/obp-api/src/main/scala/code/api/util/APIUtil.scala index 66b9d5aee5..5f2994dfd1 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -1092,6 +1092,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ } yield deleted case "status" => Full(OBPStatus(values.head)) case "consumer_id" => Full(OBPConsumerId(values.head)) + case "consent_id" => Full(OBPConsentId(values.head)) case "user_id" => Full(OBPUserId(values.head)) case "bank_id" => Full(OBPBankId(values.head)) case "account_id" => Full(OBPAccountId(values.head)) @@ -1139,6 +1140,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ anon <- getHttpParamValuesByName(httpParams,"anon") deletedStatus <- getHttpParamValuesByName(httpParams,"is_deleted") consumerId <- getHttpParamValuesByName(httpParams,"consumer_id") + consentId <- getHttpParamValuesByName(httpParams,"consent_id") userId <- getHttpParamValuesByName(httpParams, "user_id") bankId <- getHttpParamValuesByName(httpParams, "bank_id") accountId <- getHttpParamValuesByName(httpParams, "account_id") @@ -1176,7 +1178,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ val ordering = OBPOrdering(sortBy, sortDirection) //This guarantee the order List(limit, offset, ordering, fromDate, toDate, - anon, status, consumerId, userId, url, appName, implementedByPartialFunction, implementedInVersion, + anon, status, consumerId, consentId, userId, url, appName, implementedByPartialFunction, implementedInVersion, verb, correlationId, duration, excludeAppNames, excludeUrlPattern, excludeImplementedByPartialfunctions, includeAppNames, includeUrlPattern, includeImplementedByPartialfunctions, connectorName,functionName, bankId, accountId, customerId, lockedStatus, deletedStatus @@ -1214,6 +1216,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ val status = getHttpRequestUrlParam(httpRequestUrl,"status") val isDeleted = getHttpRequestUrlParam(httpRequestUrl, "is_deleted") val consumerId = getHttpRequestUrlParam(httpRequestUrl,"consumer_id") + val consentId = getHttpRequestUrlParam(httpRequestUrl,"consent_id") val userId = getHttpRequestUrlParam(httpRequestUrl, "user_id") val bankId = getHttpRequestUrlParam(httpRequestUrl, "bank_id") val accountId = getHttpRequestUrlParam(httpRequestUrl, "account_id") @@ -1244,7 +1247,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ Full(List( HTTPParam("sort_direction",sortDirection), HTTPParam("from_date",fromDate), HTTPParam("to_date", toDate), HTTPParam("limit",limit), HTTPParam("offset",offset), - HTTPParam("anon", anon), HTTPParam("status", status), HTTPParam("consumer_id", consumerId), HTTPParam("user_id", userId), HTTPParam("url", url), HTTPParam("app_name", appName), + HTTPParam("anon", anon), HTTPParam("status", status), HTTPParam("consumer_id", consumerId), HTTPParam("consent_id", consentId), HTTPParam("user_id", userId), HTTPParam("url", url), HTTPParam("app_name", appName), HTTPParam("implemented_by_partial_function",implementedByPartialFunction), HTTPParam("implemented_in_version",implementedInVersion), HTTPParam("verb", verb), HTTPParam("correlation_id", correlationId), HTTPParam("duration", duration), HTTPParam("exclude_app_names", excludeAppNames), HTTPParam("exclude_url_patterns", excludeUrlPattern),HTTPParam("exclude_implemented_by_partial_functions", excludeImplementedByPartialfunctions), diff --git a/obp-api/src/main/scala/code/api/util/OBPParam.scala b/obp-api/src/main/scala/code/api/util/OBPParam.scala index 0419d09510..7e682cc2ac 100644 --- a/obp-api/src/main/scala/code/api/util/OBPParam.scala +++ b/obp-api/src/main/scala/code/api/util/OBPParam.scala @@ -26,6 +26,7 @@ case class OBPFromDate(value: Date) extends OBPQueryParam case class OBPToDate(value: Date) extends OBPQueryParam case class OBPOrdering(field: Option[String], order: OBPOrder) extends OBPQueryParam case class OBPConsumerId(value: String) extends OBPQueryParam +case class OBPConsentId(value: String) extends OBPQueryParam case class OBPUserId(value: String) extends OBPQueryParam case class OBPStatus(value: String) extends OBPQueryParam case class OBPBankId(value: String) extends OBPQueryParam diff --git a/obp-api/src/main/scala/code/consent/MappedConsent.scala b/obp-api/src/main/scala/code/consent/MappedConsent.scala index 34fdd97c64..4f74436c70 100644 --- a/obp-api/src/main/scala/code/consent/MappedConsent.scala +++ b/obp-api/src/main/scala/code/consent/MappedConsent.scala @@ -1,7 +1,7 @@ package code.consent import java.util.Date -import code.api.util.{APIUtil, Consent, ErrorMessages, OBPStatus, OBPOffset, OBPQueryParam, OBPUserId, OBPLimit, OBPConsumerId, SecureRandomUtil} +import code.api.util.{APIUtil, Consent, ErrorMessages, OBPConsentId, OBPConsumerId, OBPLimit, OBPOffset, OBPQueryParam, OBPStatus, OBPUserId, SecureRandomUtil} import code.consent.ConsentStatus.ConsentStatus import code.model.Consumer import code.util.MappedUUID @@ -67,8 +67,9 @@ object MappedConsentProvider extends ConsentProvider { private def getQueryParams(queryParams: List[OBPQueryParam]) = { val limit = queryParams.collect { case OBPLimit(value) => MaxRows[MappedConsent](value) }.headOption val offset = queryParams.collect { case OBPOffset(value) => StartAt[MappedConsent](value) }.headOption - // he optional variables: + // The optional variables: val consumerId = queryParams.collect { case OBPConsumerId(value) => By(MappedConsent.mConsumerId, value)}.headOption + val consentId = queryParams.collect { case OBPConsentId(value) => By(MappedConsent.mConsentId, value)}.headOption val userId = queryParams.collect { case OBPUserId(value) => By(MappedConsent.mUserId, value)}.headOption val status = queryParams.collect { case OBPStatus(value) => By(MappedConsent.mStatus, value.toUpperCase())}.headOption @@ -77,6 +78,7 @@ object MappedConsentProvider extends ConsentProvider { limit.toSeq, status.toSeq, userId.toSeq, + consentId.toSeq, consumerId.toSeq ).flatten } From d283e64a327fb8362512b218fb0a0359e49e1706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 10 Dec 2024 10:39:24 +0100 Subject: [PATCH 5/5] feature/Improve integrate_with_hydra props behaviour --- obp-api/src/main/scala/code/api/OAuth2.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obp-api/src/main/scala/code/api/OAuth2.scala b/obp-api/src/main/scala/code/api/OAuth2.scala index 83e32bbc57..ada1210b7e 100644 --- a/obp-api/src/main/scala/code/api/OAuth2.scala +++ b/obp-api/src/main/scala/code/api/OAuth2.scala @@ -323,7 +323,7 @@ object OAuth2Login extends RestHelper with MdcLoggable { } def resolveProvider(idToken: String) = { - isIssuer(jwtToken = idToken, identityProvider = hydraPublicUrl) match { + HydraUtil.integrateWithHydra && isIssuer(jwtToken = idToken, identityProvider = hydraPublicUrl) match { case true if HydraUtil.hydraUsesObpUserCredentials => // Case that source of the truth of Hydra user management is the OBP-API mapper DB // In case that ORY Hydra login url is "hostname/user_mgt/login" we MUST override hydraPublicUrl as provider // in order to avoid creation of a new user