diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 3b0478bb5..511663780 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -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 \ No newline at end of file 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 96134b20a..e394d1a78 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -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]] diff --git a/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala b/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala index e23e96eea..ef3b22944 100644 --- a/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala +++ b/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala @@ -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)) diff --git a/obp-api/src/main/scala/code/api/v5_0_0/APIMethods500.scala b/obp-api/src/main/scala/code/api/v5_0_0/APIMethods500.scala index 5c9894d1e..51ff0b6f7 100644 --- a/obp-api/src/main/scala/code/api/v5_0_0/APIMethods500.scala +++ b/obp-api/src/main/scala/code/api/v5_0_0/APIMethods500.scala @@ -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)) } diff --git a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala index cc86dfa46..f70cfb458 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala @@ -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)) } } } diff --git a/obp-api/src/main/scala/code/bankconnectors/Connector.scala b/obp-api/src/main/scala/code/bankconnectors/Connector.scala index 036a4fb97..8ac672533 100644 --- a/obp-api/src/main/scala/code/bankconnectors/Connector.scala +++ b/obp-api/src/main/scala/code/bankconnectors/Connector.scala @@ -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, diff --git a/release_notes.md b/release_notes.md index be33b2fa7..63b8a3f1b 100644 --- a/release_notes.md +++ b/release_notes.md @@ -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.