Merge pull request #2483 from hongwei1/develop

feature/added skip_consent_sca_for_consumer_id_pairs
This commit is contained in:
Simon Redfern 2025-01-24 16:08:37 +01:00 committed by GitHub
commit a3ab95af60
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 250 additions and 150 deletions

View File

@ -1351,5 +1351,13 @@ validate_iban=false
# sample props regulated_entities = [{"certificate_authority_ca_owner_id":"CY_CBC","entity_certificate_public_key":"-----BEGIN CERTIFICATE-----MIICsjCCAZqgAwIBAgIGAYwQ62R0MA0GCSqGSIb3DQEBCwUAMBoxGDAWBgNVBAMMD2FwcC5leGFtcGxlLmNvbTAeFw0yMzExMjcxMzE1MTFaFw0yNTExMjYxMzE1MTFaMBoxGDAWBgNVBAMMD2FwcC5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK9WIodZHWzKyCcf9YfWEhPURbfO6zKuMqzHN27GdqHsVVEGxP4F/J4mso+0ENcRr6ur4u81iREaVdCc40rHDHVJNEtniD8Icbz7tcsqAewIVhc/q6WXGqImJpCq7hA0m247dDsaZT0lb/MVBiMoJxDEmAE/GYYnWTEn84R35WhJsMvuQ7QmLvNg6RkChY6POCT/YKe9NKwa1NqI1U+oA5RFzAaFtytvZCE3jtp+aR0brL7qaGfgxm6B7dEpGyhg0NcVCV7xMQNq2JxZTVdAr6lcsRGaAFulakmW3aNnmK+L35Wu8uW+OxNxwUuC6f3b4FVBa276FMuUTRfu7gc+k6kCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAAU5CjEyAoyTn7PgFpQD48ZNPuUsEQ19gzYgJvHMzFIoZ7jKBodjO5mCzWBcR7A4mpeAsdyiNBl2sTiZscSnNqxk61jVzP5Ba1D7XtOjjr7+3iqowrThj6BY40QqhYh/6BSY9fDzVZQiHnvlo6ZUM5kUK6OavZOovKlp5DIl5sGqoP0qAJnpQ4nhB2WVVsKfPlOXc+2KSsbJ23g9l8zaTMr+X0umlvfEKqyEl1Fa2L1dO0y/KFQ+ILmxcZLpRdq1hRAjd0quq9qGC8ucXhRWDgM4hslVpau0da68g0aItWNez3mc5lB82b3dcZpFMzO41bgw7gvw10AvvTfQDqEYIuQ==-----END CERTIFICATE-----","entity_code":"PSD_PICY_CBC!12345","entity_type":"PSD_PI","entity_address":"EXAMPLE COMPANY LTD, 5 SOME STREET","entity_town_city":"SOME CITY","entity_post_code":"1060","entity_country":"CY","entity_web_site":"www.example.com","services":[{"CY":["PS_010","PS_020","PS_03C","PS_04C"]}]}]
regulated_entities = []
#In OBP Create Consent if the app that is creating the consent (grantor_consumer_id) wants to create a consent for the grantee_consumer_id App, then we should skip SCA.
#The use case is API Explorer II giving a consent to Opey . In such a case API Explorer II and Opey are effectively the same App as far as the user is concerned.
#skip_consent_sca_for_consumer_id_pairs=[{ \
# "grantor_consumer_id": "ef0a8fa4-3814-4a21-8ca9-8c553a43aa631", \
# "grantee_consumer_id": "fb327484-94d7-44d2-83e5-8d27301e8279" \
#}]
# Note: For secure and http only settings for cookies see resources/web.xml which is mentioned in the README.md

View File

@ -601,6 +601,11 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
domain: String,
bank_ids: List[String]
)
//This is used for get the value from props `skip_consent_sca_for_consumer_id_pairs`
case class ConsumerIdPair(
grantor_consumer_id: String,
grantee_consumer_id: String
)
case class EmailDomainToEntitlementMapping(
domain: String,
@ -4729,6 +4734,23 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
APIUtil.getPropsValue("email_domain_to_space_mappings").map(extractor).getOrElse(Nil)
}
val skipConsentScaForConsumerIdPairs: List[ConsumerIdPair] = {
def extractor(str: String) = try {
val consumerIdPair = json.parse(str).extract[List[ConsumerIdPair]]
//The props value can be parsed to JNothing.
if(str.nonEmpty && consumerIdPair == Nil)
throw new RuntimeException("props [skip_consent_sca_for_consumer_id_pairs] parse -> extract to Nil!")
else
consumerIdPair
} catch {
case e: Throwable => // error handling, found wrong props value as early as possible.
this.logger.error(s"props [skip_consent_sca_for_consumer_id_pairs] value is invalid, it should be the class($ConsumerIdPair) json format, current value is $str ." );
throw e;
}
APIUtil.getPropsValue("skip_consent_sca_for_consumer_id_pairs").map(extractor).getOrElse(Nil)
}
val emailDomainToEntitlementMappings: List[EmailDomainToEntitlementMapping] = {
def extractor(str: String) = try {
val emailDomainToEntitlementMappings = json.parse(str).extract[List[EmailDomainToEntitlementMapping]]

View File

@ -28,7 +28,7 @@ import code.api.v3_0_0.JSONFactory300.createAdapterInfoJson
import code.api.v3_1_0.JSONFactory310._
import code.bankconnectors.rest.RestConnector_vMar2019
import code.bankconnectors.{Connector, LocalMappedConnector}
import code.consent.{ConsentRequests, ConsentStatus, Consents}
import code.consent.{ConsentRequests, ConsentStatus, Consents, MappedConsent}
import code.consumer.Consumers
import code.context.UserAuthContextUpdateProvider
import code.entitlement.Entitlement
@ -57,6 +57,7 @@ import net.liftweb.http.provider.HTTPParam
import net.liftweb.http.rest.RestHelper
import net.liftweb.json._
import net.liftweb.util.Helpers.tryo
import net.liftweb.mapper.By
import net.liftweb.util.Mailer.{From, PlainMailBodyType, Subject, To}
import net.liftweb.util.{Helpers, Mailer, Props, StringHelpers}
import org.apache.commons.lang3.{StringUtils, Validate}
@ -3586,66 +3587,82 @@ trait APIMethods310 {
_ <- Future(Consents.consentProvider.vend.setJsonWebToken(createdConsent.consentId, consentJWT)) map {
i => connectorEmptyResponse(i, callContext)
}
challengeText = s"Your consent challenge : ${challengeAnswer}, Application: $applicationText"
_ <- scaMethod match {
case v if v == StrongCustomerAuthentication.EMAIL.toString => // Send the email
for{
failMsg <- Future {s"$InvalidJsonFormat The Json body should be the $PostConsentEmailJsonV310"}
postConsentEmailJson <- NewStyle.function.tryons(failMsg, 400, callContext) {
json.extract[PostConsentEmailJsonV310]
}
(status, callContext) <- NewStyle.function.sendCustomerNotification(
StrongCustomerAuthentication.EMAIL,
postConsentEmailJson.email,
Some("OBP Consent Challenge"),
challengeText,
callContext
)
} yield Future{status}
case v if v == StrongCustomerAuthentication.SMS.toString =>
for {
failMsg <- Future {
s"$InvalidJsonFormat The Json body should be the $PostConsentPhoneJsonV310"
}
postConsentPhoneJson <- NewStyle.function.tryons(failMsg, 400, callContext) {
json.extract[PostConsentPhoneJsonV310]
}
phoneNumber = postConsentPhoneJson.phone_number
(status, callContext) <- NewStyle.function.sendCustomerNotification(
StrongCustomerAuthentication.SMS,
phoneNumber,
None,
challengeText,
callContext
)
} yield Future{status}
case v if v == StrongCustomerAuthentication.IMPLICIT.toString =>
for {
(consentImplicitSCA, callContext) <- NewStyle.function.getConsentImplicitSCA(user, callContext)
status <- consentImplicitSCA.scaMethod match {
case v if v == StrongCustomerAuthentication.EMAIL => // Send the email
NewStyle.function.sendCustomerNotification (
StrongCustomerAuthentication.EMAIL,
consentImplicitSCA.recipient,
Some ("OBP Consent Challenge"),
challengeText,
callContext
)
case v if v == StrongCustomerAuthentication.SMS =>
NewStyle.function.sendCustomerNotification(
StrongCustomerAuthentication.SMS,
consentImplicitSCA.recipient,
None,
challengeText,
callContext
)
case _ => Future {
"Success"
}
}} yield {
status
//we need to check `skip_consent_sca_for_consumer_id_pairs` props, to see if we really need the SCA flow.
//this is from callContext
grantorConsumerId = callContext.map(_.consumer.toOption.map(_.consumerId.get)).flatten.getOrElse("Unknown")
//this is from json body
granteeConsumerId = consentJson.consumer_id.getOrElse("Unknown")
shouldSkipConsentScaForConsumerIdPair = APIUtil.skipConsentScaForConsumerIdPairs.contains(
APIUtil.ConsumerIdPair(
grantorConsumerId,
granteeConsumerId
))
mappedConsent <- if (shouldSkipConsentScaForConsumerIdPair) {
Future{
MappedConsent.find(By(MappedConsent.mConsentId, createdConsent.consentId)).map(_.mStatus(ConsentStatus.ACCEPTED.toString).saveMe()).head
}
case _ =>Future{"Success"}
} else {
val challengeText = s"Your consent challenge : ${challengeAnswer}, Application: $applicationText"
scaMethod match {
case v if v == StrongCustomerAuthentication.EMAIL.toString => // Send the email
for{
failMsg <- Future {s"$InvalidJsonFormat The Json body should be the $PostConsentEmailJsonV310"}
postConsentEmailJson <- NewStyle.function.tryons(failMsg, 400, callContext) {
json.extract[PostConsentEmailJsonV310]
}
(status, callContext) <- NewStyle.function.sendCustomerNotification(
StrongCustomerAuthentication.EMAIL,
postConsentEmailJson.email,
Some("OBP Consent Challenge"),
challengeText,
callContext
)
} yield createdConsent
case v if v == StrongCustomerAuthentication.SMS.toString =>
for {
failMsg <- Future {
s"$InvalidJsonFormat The Json body should be the $PostConsentPhoneJsonV310"
}
postConsentPhoneJson <- NewStyle.function.tryons(failMsg, 400, callContext) {
json.extract[PostConsentPhoneJsonV310]
}
phoneNumber = postConsentPhoneJson.phone_number
(status, callContext) <- NewStyle.function.sendCustomerNotification(
StrongCustomerAuthentication.SMS,
phoneNumber,
None,
challengeText,
callContext
)
} yield createdConsent
case v if v == StrongCustomerAuthentication.IMPLICIT.toString =>
for {
(consentImplicitSCA, callContext) <- NewStyle.function.getConsentImplicitSCA(user, callContext)
status <- consentImplicitSCA.scaMethod match {
case v if v == StrongCustomerAuthentication.EMAIL => // Send the email
NewStyle.function.sendCustomerNotification (
StrongCustomerAuthentication.EMAIL,
consentImplicitSCA.recipient,
Some ("OBP Consent Challenge"),
challengeText,
callContext
)
case v if v == StrongCustomerAuthentication.SMS =>
NewStyle.function.sendCustomerNotification(
StrongCustomerAuthentication.SMS,
consentImplicitSCA.recipient,
None,
challengeText,
callContext
)
case _ => Future {
"Success"
}
}} yield {
createdConsent
}
case _ =>Future{createdConsent}}
}
} yield {
(ConsentJsonV310(createdConsent.consentId, consentJWT, createdConsent.status), HttpCode.`201`(callContext))

View File

@ -18,7 +18,7 @@ import code.api.v4_0_0.{JSONFactory400, PostCounterpartyJson400}
import code.api.v5_0_0.JSONFactory500.{createPhysicalCardJson, createViewJsonV500, createViewsIdsJsonV500, createViewsJsonV500}
import code.api.v5_1_0.{CreateCustomViewJson, PostCounterpartyLimitV510, PostVRPConsentRequestJsonV510}
import code.bankconnectors.Connector
import code.consent.{ConsentRequests, Consents}
import code.consent.{ConsentRequests, ConsentStatus, Consents, MappedConsent}
import code.consumer.Consumers
import code.entitlement.Entitlement
import code.metadata.counterparties.MappedCounterparty
@ -40,6 +40,7 @@ import net.liftweb.json
import net.liftweb.json.{Extraction, compactRender, prettyRender}
import net.liftweb.util.Helpers.tryo
import net.liftweb.util.{Helpers, Props, StringHelpers}
import net.liftweb.mapper.By
import java.util.UUID
import java.util.concurrent.ThreadLocalRandom
@ -1202,34 +1203,54 @@ trait APIMethods500 {
_ <- Future(Consents.consentProvider.vend.setJsonWebToken(createdConsent.consentId, consentJWT)) map {
i => connectorEmptyResponse(i, callContext)
}
challengeText = s"Your consent challenge : ${challengeAnswer}, Application: $applicationText"
_ <- scaMethod match {
case v if v == StrongCustomerAuthentication.EMAIL.toString => // Send the email
sendEmailNotification(callContext, consentRequestJson, challengeText)
case v if v == StrongCustomerAuthentication.SMS.toString =>
sendSmsNotification(callContext, consentRequestJson, challengeText)
case v if v == StrongCustomerAuthentication.IMPLICIT.toString =>
for {
(consentImplicitSCA, callContext) <- NewStyle.function.getConsentImplicitSCA(user, callContext)
status <- consentImplicitSCA.scaMethod match {
case v if v == StrongCustomerAuthentication.EMAIL => // Send the email
sendEmailNotification(callContext, consentRequestJson.copy(email=Some(consentImplicitSCA.recipient)), challengeText)
case v if v == StrongCustomerAuthentication.SMS =>
sendSmsNotification(callContext, consentRequestJson.copy(phone_number=Some(consentImplicitSCA.recipient)), challengeText)
case _ => Future {
"Success"
}
}} yield {
status
//we need to check `skip_consent_sca_for_consumer_id_pairs` props, to see if we really need the SCA flow.
//this is from callContext
grantorConsumerId = callContext.map(_.consumer.toOption.map(_.consumerId.get)).flatten.getOrElse("Unknown")
//this is from json body
granteeConsumerId = postConsentBodyCommonJson.consumer_id.getOrElse("Unknown")
shouldSkipConsentScaForConsumerIdPair = APIUtil.skipConsentScaForConsumerIdPairs.contains(
APIUtil.ConsumerIdPair(
grantorConsumerId,
granteeConsumerId
))
mappedConsent <- if (shouldSkipConsentScaForConsumerIdPair) {
Future{
MappedConsent.find(By(MappedConsent.mConsentId, createdConsent.consentId)).map(_.mStatus(ConsentStatus.ACCEPTED.toString).saveMe()).head
}
} else {
val challengeText = s"Your consent challenge : ${challengeAnswer}, Application: $applicationText"
scaMethod match {
case v if v == StrongCustomerAuthentication.EMAIL.toString => // Send the email
sendEmailNotification(callContext, consentRequestJson, challengeText)
case v if v == StrongCustomerAuthentication.SMS.toString =>
sendSmsNotification(callContext, consentRequestJson, challengeText)
case v if v == StrongCustomerAuthentication.IMPLICIT.toString =>
for {
(consentImplicitSCA, callContext) <- NewStyle.function.getConsentImplicitSCA(user, callContext)
status <- consentImplicitSCA.scaMethod match {
case v if v == StrongCustomerAuthentication.EMAIL => // Send the email
sendEmailNotification(callContext, consentRequestJson.copy(email = Some(consentImplicitSCA.recipient)), challengeText)
case v if v == StrongCustomerAuthentication.SMS =>
sendSmsNotification(callContext, consentRequestJson.copy(phone_number = Some(consentImplicitSCA.recipient)), challengeText)
case _ => Future {
"Success"
}
}} yield {
status
}
case _ => Future {
"Success"
}
case _ =>Future{"Success"}
}
Future{createdConsent}
}
} yield {
(ConsentJsonV500(
createdConsent.consentId,
mappedConsent.consentId,
consentJWT,
createdConsent.status,
Some(createdConsent.consentRequestId),
mappedConsent.status,
Some(mappedConsent.consentRequestId),
if (isVRPConsentRequest) Some(ConsentAccountAccessJson(bankId.value, accountId.value, viewId.value, HelperInfoJson(List(counterpartyId.value)))) else None
), HttpCode.`201`(callContext))
}

View File

@ -2050,77 +2050,95 @@ trait APIMethods510 {
_ <- Future(Consents.consentProvider.vend.setJsonWebToken(createdConsent.consentId, consentJWT)) map {
i => connectorEmptyResponse(i, callContext)
}
challengeText = s"Your consent challenge : ${challengeAnswer}, Application: $applicationText"
_ <- scaMethod match {
case v if v == StrongCustomerAuthentication.EMAIL.toString => // Send the email
for {
failMsg <- Future {
s"$InvalidJsonFormat The Json body should be the $PostConsentEmailJsonV310"
}
postConsentEmailJson <- NewStyle.function.tryons(failMsg, 400, callContext) {
json.extract[PostConsentEmailJsonV310]
}
(status, callContext) <- NewStyle.function.sendCustomerNotification(
StrongCustomerAuthentication.EMAIL,
postConsentEmailJson.email,
Some("OBP Consent Challenge"),
challengeText,
callContext
)
} yield Future {
status
}
case v if v == StrongCustomerAuthentication.SMS.toString =>
for {
failMsg <- Future {
s"$InvalidJsonFormat The Json body should be the $PostConsentPhoneJsonV310"
}
postConsentPhoneJson <- NewStyle.function.tryons(failMsg, 400, callContext) {
json.extract[PostConsentPhoneJsonV310]
}
phoneNumber = postConsentPhoneJson.phone_number
(status, callContext) <- NewStyle.function.sendCustomerNotification(
StrongCustomerAuthentication.SMS,
phoneNumber,
None,
challengeText,
callContext
)
} yield Future {
status
}
case v if v == StrongCustomerAuthentication.IMPLICIT.toString =>
for {
(consentImplicitSCA, callContext) <- NewStyle.function.getConsentImplicitSCA(user, callContext)
status <- consentImplicitSCA.scaMethod match {
case v if v == StrongCustomerAuthentication.EMAIL => // Send the email
NewStyle.function.sendCustomerNotification(
StrongCustomerAuthentication.EMAIL,
consentImplicitSCA.recipient,
Some("OBP Consent Challenge"),
challengeText,
callContext
)
case v if v == StrongCustomerAuthentication.SMS =>
NewStyle.function.sendCustomerNotification(
StrongCustomerAuthentication.SMS,
consentImplicitSCA.recipient,
None,
challengeText,
callContext
)
case _ => Future {
"Success"
//we need to check `skip_consent_sca_for_consumer_id_pairs` props, to see if we really need the SCA flow.
//this is from callContext
grantorConsumerId = callContext.map(_.consumer.toOption.map(_.consumerId.get)).flatten.getOrElse("Unknown")
//this is from json body
granteeConsumerId = consentJson.consumer_id.getOrElse("Unknown")
shouldSkipConsentScaForConsumerIdPair = APIUtil.skipConsentScaForConsumerIdPairs.contains(
APIUtil.ConsumerIdPair(
grantorConsumerId,
granteeConsumerId
))
mappedConsent <- if (shouldSkipConsentScaForConsumerIdPair) {
Future{
MappedConsent.find(By(MappedConsent.mConsentId, createdConsent.consentId)).map(_.mStatus(ConsentStatus.ACCEPTED.toString).saveMe()).head
}
} else {
val challengeText = s"Your consent challenge : ${challengeAnswer}, Application: $applicationText"
scaMethod match {
case v if v == StrongCustomerAuthentication.EMAIL.toString => // Send the email
for {
failMsg <- Future {
s"$InvalidJsonFormat The Json body should be the $PostConsentEmailJsonV310"
}
}} yield {
status
postConsentEmailJson <- NewStyle.function.tryons(failMsg, 400, callContext) {
json.extract[PostConsentEmailJsonV310]
}
(status, callContext) <- NewStyle.function.sendCustomerNotification(
StrongCustomerAuthentication.EMAIL,
postConsentEmailJson.email,
Some("OBP Consent Challenge"),
challengeText,
callContext
)
} yield {
createdConsent
}
case v if v == StrongCustomerAuthentication.SMS.toString =>
for {
failMsg <- Future {
s"$InvalidJsonFormat The Json body should be the $PostConsentPhoneJsonV310"
}
postConsentPhoneJson <- NewStyle.function.tryons(failMsg, 400, callContext) {
json.extract[PostConsentPhoneJsonV310]
}
phoneNumber = postConsentPhoneJson.phone_number
(status, callContext) <- NewStyle.function.sendCustomerNotification(
StrongCustomerAuthentication.SMS,
phoneNumber,
None,
challengeText,
callContext
)
} yield {
createdConsent
}
case v if v == StrongCustomerAuthentication.IMPLICIT.toString =>
for {
(consentImplicitSCA, callContext) <- NewStyle.function.getConsentImplicitSCA(user, callContext)
status <- consentImplicitSCA.scaMethod match {
case v if v == StrongCustomerAuthentication.EMAIL => // Send the email
NewStyle.function.sendCustomerNotification(
StrongCustomerAuthentication.EMAIL,
consentImplicitSCA.recipient,
Some("OBP Consent Challenge"),
challengeText,
callContext
)
case v if v == StrongCustomerAuthentication.SMS =>
NewStyle.function.sendCustomerNotification(
StrongCustomerAuthentication.SMS,
consentImplicitSCA.recipient,
None,
challengeText,
callContext
)
case _ => Future {
"Success"
}
}} yield {
createdConsent
}
case _ => Future {
createdConsent
}
case _ => Future {
"Success"
}
}
} yield {
(ConsentJsonV310(createdConsent.consentId, consentJWT, createdConsent.status), HttpCode.`201`(callContext))
(ConsentJsonV310(mappedConsent.consentId, consentJWT, mappedConsent.status), HttpCode.`201`(callContext))
}
}
}

View File

@ -303,6 +303,19 @@ trait Connector extends MdcLoggable {
callContext: Option[CallContext]
): OBPReturnType[Box[AmountOfMoney]] =Future{(Failure(setUnimplementedError(nameOf(getChallengeThreshold _))), callContext)}
//TODO. WIP
def shouldRaiseConsentChallenge(
bankId: String,
accountId: String,
viewId: String,
consentRequestType: String,
userId: String,
username: String,
callContext: Option[CallContext]
): OBPReturnType[Box[(Boolean, String)]] = Future {
(Failure(setUnimplementedError(nameOf(shouldRaiseConsentChallenge _))), callContext)
}
def getPaymentLimit(
bankId: String,
accountId: String,

View File

@ -3,6 +3,7 @@
### Most recent changes at top of file
```
Date Commit Action
24/01/2025 ad68f054 Added props skip_consent_sca_for_consumer_id_pairs .
03/02/2024 7bcb6bc5 Added props oauth2.keycloak.source_of_truth, default is false.
oauth2.keycloak.source_of_truth = true turns sync ON.
It is used to sync IAM of OBP-API and IAM of Keycloak.