refactor/put challenge into MappedExpectedChallengeAnswer table.

This commit is contained in:
hongwei 2022-06-02 01:07:48 +02:00
parent 725b910498
commit 435de19d4a
7 changed files with 127 additions and 122 deletions

View File

@ -72,6 +72,7 @@ import code.dynamicMessageDoc.{DynamicMessageDocProvider, JsonDynamicMessageDoc}
import code.dynamicResourceDoc.{DynamicResourceDocProvider, JsonDynamicResourceDoc}
import code.endpointMapping.{EndpointMappingProvider, EndpointMappingT}
import code.endpointTag.EndpointTagT
import code.transactionrequests.TransactionRequests.TransactionChallengeTypes
import code.util.Helper.MdcLoggable
import code.views.system.AccountAccess
import net.liftweb.mapper.By
@ -1154,7 +1155,7 @@ object NewStyle extends MdcLoggable{
transactionRequestCommonBody: TransactionRequestCommonBodyJSON,
detailsPlain: String,
chargePolicy: String,
challengeType: Option[String],
challengeType: Option[TransactionChallengeTypes.Value],
scaMethod: Option[SCA],
reasons: Option[List[TransactionRequestReason]],
berlinGroupPayments: Option[SepaCreditTransfersBerlinGroupV13],
@ -1169,7 +1170,7 @@ object NewStyle extends MdcLoggable{
transactionRequestCommonBody: TransactionRequestCommonBodyJSON,
detailsPlain: String,
chargePolicy: String,
challengeType: Option[String],
challengeType = challengeType.map(_.toString),
scaMethod: Option[SCA],
reasons: Option[List[TransactionRequestReason]],
berlinGroupPayments: Option[SepaCreditTransfersBerlinGroupV13],
@ -1334,6 +1335,23 @@ object NewStyle extends MdcLoggable{
Connector.connector.vend.validateChallengeAnswer(challengeId: String, hashOfSuppliedAnswer: String, callContext: Option[CallContext]) map { i =>
(unboxFullOrFail(i._1, callContext, s"$InvalidChallengeAnswer "), i._2)
}
def allChallengesSuccessfullyAnswered(
bankId: BankId,
accountId: AccountId,
transReqId: TransactionRequestId,
challenges: List[ChallengeTrait],
callContext: Option[CallContext]
): OBPReturnType[Boolean] =
Connector.connector.vend.allChallengesSuccessfullyAnswered(
bankId: BankId,
accountId: AccountId,
transReqId: TransactionRequestId,
challenges: List[ChallengeTrait],
callContext: Option[CallContext]
) map { i =>
(unboxFullOrFail(i._1, callContext, s"$InvalidConnectorResponse"), i._2)
}
def validateAndCheckIbanNumber(iban: String, callContext: Option[CallContext]): OBPReturnType[IbanChecker] =
Connector.connector.vend.validateAndCheckIbanNumber(iban: String, callContext: Option[CallContext]) map { i =>

View File

@ -1035,7 +1035,7 @@ trait APIMethods400 {
transactionRequestBodyRefundJson.copy(description = newDescription),
transDetailsSerialized,
sharedChargePolicy.toString,
Some(OTP_VIA_API.toString),
Some(OTP_VIA_API),
getScaMethodAtInstance(transactionRequestType.value).toOption,
None,
None,
@ -1091,7 +1091,7 @@ trait APIMethods400 {
transactionRequestBodySandboxTan,
transDetailsSerialized,
sharedChargePolicy.toString,
Some(OTP_VIA_API.toString),
Some(OTP_VIA_API),
getScaMethodAtInstance(transactionRequestType.value).toOption,
None,
None,
@ -1120,7 +1120,7 @@ trait APIMethods400 {
transactionRequestBodySandboxTan,
transDetailsSerialized,
sharedChargePolicy.toString,
Some(OTP_VIA_WEB_FORM.toString),
Some(OTP_VIA_WEB_FORM),
getScaMethodAtInstance(transactionRequestType.value).toOption,
None,
None,
@ -1155,7 +1155,7 @@ trait APIMethods400 {
transactionRequestBodyCounterparty,
transDetailsSerialized,
chargePolicy,
Some(OTP_VIA_API.toString),
Some(OTP_VIA_API),
getScaMethodAtInstance(transactionRequestType.value).toOption,
None,
None,
@ -1207,7 +1207,7 @@ trait APIMethods400 {
transactionRequestBodySimple,
transDetailsSerialized,
chargePolicy,
Some(OTP_VIA_API.toString),
Some(OTP_VIA_API),
getScaMethodAtInstance(transactionRequestType.value).toOption,
None,
None,
@ -1242,7 +1242,7 @@ trait APIMethods400 {
transDetailsSEPAJson,
transDetailsSerialized,
chargePolicy,
Some(OTP_VIA_API.toString),
Some(OTP_VIA_API),
getScaMethodAtInstance(transactionRequestType.value).toOption,
transDetailsSEPAJson.reasons.map(_.map(_.transform)),
None,
@ -1267,7 +1267,7 @@ trait APIMethods400 {
transactionRequestBodyFreeForm,
transDetailsSerialized,
sharedChargePolicy.toString,
Some(OTP_VIA_API.toString),
Some(OTP_VIA_API),
getScaMethodAtInstance(transactionRequestType.value).toOption,
None,
None,
@ -1276,31 +1276,8 @@ trait APIMethods400 {
(createdTransactionRequest, callContext)
}
}
(challenges, callContext) <- NewStyle.function.getChallengesByTransactionRequestId(createdTransactionRequest.id.value, callContext)
} yield {
//TODO, remove this `connector` guard logic, the challenges should come from other places.
// The OBP mapped V400 payment.challenges are not done yet, the we should use `createChallengesC2` instead of `createChallenges` in createTransactionRequestv400 method,
// and get the challenges from connector level, not prepare them here.
val challenges : List[ChallengeJson] = if(APIUtil.getPropsValue("connector").openOrThrowException(attemptedToOpenAnEmptyBox).toString.equalsIgnoreCase("mapped")){
MappedExpectedChallengeAnswer
.findAll(By(MappedExpectedChallengeAnswer.mTransactionRequestId, createdTransactionRequest.id.value))
.map(mappedExpectedChallengeAnswer =>
ChallengeJson(
mappedExpectedChallengeAnswer.challengeId,
mappedExpectedChallengeAnswer.transactionRequestId,
mappedExpectedChallengeAnswer.expectedUserId,
mappedExpectedChallengeAnswer.attemptCounter
))
} else {
if(!("COMPLETED").equals(createdTransactionRequest.status))
List(ChallengeJson(
createdTransactionRequest.challenge.id,
createdTransactionRequest.id.value,
u.userId,
createdTransactionRequest.challenge.allowed_attempts
))
else
null
}
(JSONFactory400.createTransactionRequestWithChargeJSON(createdTransactionRequest, challenges), HttpCode.`201`(callContext))
}
}
@ -1415,7 +1392,9 @@ trait APIMethods400 {
OTP_VIA_WEB_FORM.toString
).exists(_ == existingTransactionRequest.challenge.challenge_type)
}
(challenges, callContext) <- NewStyle.function.getChallengesByTransactionRequestId(transReqId.value, callContext)
(transactionRequest, callContext) <- challengeAnswerJson.answer match {
// If the challenge answer is `REJECT` - Currently only to Reject a SEPA transaction request REFUND
case "REJECT" =>
@ -1467,47 +1446,17 @@ trait APIMethods400 {
} yield (transactionRequest, callContext)
case _ =>
for {
// Check the challengeId is valid for this existingTransactionRequest
_ <- Helper.booleanToFuture(s"${InvalidTransactionRequestChallengeId}", cc=callContext) {
if (APIUtil.isDataFromOBPSide("validateChallengeAnswer")) {
MappedExpectedChallengeAnswer
.findAll(By(MappedExpectedChallengeAnswer.mTransactionRequestId, transReqId.value))
.exists(_.challengeId == challengeAnswerJson.id)
}else{
existingTransactionRequest.challenge.id.equals(challengeAnswerJson.id)
}
}
(challengeAnswerIsValidated, callContext) <- NewStyle.function.validateChallengeAnswer(challengeAnswerJson.id, challengeAnswerJson.answer, callContext)
_ <- Helper.booleanToFuture(s"${InvalidChallengeAnswer.replace("answer may be expired.",s"answer may be expired, current expiration time is ${transactionRequestChallengeTtl} seconds .")} ", cc=callContext) {
challengeAnswerIsValidated
}
//TODO, this is a temporary solution, we only checked single challenge Id for remote connectors. here is only for the localMapped Connector logic
_ <- if (APIUtil.isDataFromOBPSide("validateChallengeAnswer")){
for{
accountAttributes <- Connector.connector.vend.getAccountAttributesByAccount(bankId, accountId, None)
_ <- Helper.booleanToFuture(s"$NextChallengePending", cc=callContext) {
val quorum = accountAttributes._1.toList.flatten.find(_.name == "REQUIRED_CHALLENGE_ANSWERS").map(_.value).getOrElse("1").toInt
MappedExpectedChallengeAnswer
.findAll(By(MappedExpectedChallengeAnswer.mTransactionRequestId, transReqId.value))
.count(_.successful == true) match {
case number if number >= quorum => true
case _ =>
MappedTransactionRequestProvider.saveTransactionRequestStatusImpl(transReqId, TransactionRequestStatus.NEXT_CHALLENGE_PENDING.toString)
false
}
}
} yield {
true
}
} else{
Future{true}
(challengeAnswerIsValidated, callContext) <- NewStyle.function.allChallengesSuccessfullyAnswered(bankId, accountId, transReqId, challenges, callContext)
_ <- Helper.booleanToFuture(s"$NextChallengePending", cc=callContext) {
challengeAnswerIsValidated
}
// All Good, proceed with the Transaction creation...
(transactionRequest, callContext) <- TransactionRequestTypes.withName(transactionRequestType.value) match {
case TRANSFER_TO_PHONE | TRANSFER_TO_ATM | TRANSFER_TO_ACCOUNT =>
NewStyle.function.createTransactionAfterChallengeV300(u, fromAccount, transReqId, transactionRequestType, callContext)
@ -1518,7 +1467,7 @@ trait APIMethods400 {
}
} yield {
(JSONFactory210.createTransactionRequestWithChargeJSON(transactionRequest), HttpCode.`202`(callContext))
(JSONFactory400.createTransactionRequestWithChargeJSON(transactionRequest, challenges), HttpCode.`202`(callContext))
}
}
}

View File

@ -1186,7 +1186,7 @@ object JSONFactory400 {
account_attributes = accountAttributes.map(createAccountAttributeJson)
)
def createTransactionRequestWithChargeJSON(tr : TransactionRequest, challenges: List[ChallengeJson]) : TransactionRequestWithChargeJSON400 = {
def createTransactionRequestWithChargeJSON(tr : TransactionRequest, challenges: List[ChallengeTrait]) : TransactionRequestWithChargeJSON400 = {
new TransactionRequestWithChargeJSON400(
id = stringOrNull(tr.id.value),
`type` = stringOrNull(tr.`type`),
@ -1202,6 +1202,7 @@ object JSONFactory400 {
// Some (mapped) data might not have the challenge. TODO Make this nicer
challenges = {
try {
challenges.map(challenge =>{
val otpViaWebFormPath = Constant.HostName + List(
"/otp?flow=transaction_request&bankId=",
stringOrNull(tr.from.bank_id),
@ -1213,7 +1214,7 @@ object JSONFactory400 {
"&transactionRequestId=",
stringOrNull(tr.id.value),
"&id=",
stringOrNull(tr.challenge.id)
stringOrNull(challenge.challengeId)
).mkString("")
val otpViaApiPath = Constant.HostName + List(
@ -1227,14 +1228,20 @@ object JSONFactory400 {
"/transaction-requests/",
stringOrNull(tr.id.value),
"/challenge").mkString("")
val link = tr.challenge.challenge_type match {
val link = challenge.challengeType match {
case challengeType if challengeType == TransactionChallengeTypes.OTP_VIA_WEB_FORM.toString => otpViaWebFormPath
case challengeType if challengeType == TransactionChallengeTypes.OTP_VIA_API.toString => otpViaApiPath
case _ => ""
}
challenges.map(
e => ChallengeJsonV400(id = stringOrNull(e.challenge_id), user_id = e.expected_user_id, allowed_attempts = e.allowed_attempts, challenge_type = stringOrNull(tr.challenge.challenge_type), link = link)
)
}
ChallengeJsonV400(
id = stringOrNull(challenge.challengeId),
user_id = challenge.expectedUserId,
allowed_attempts = 3, //TODO, this should be fixed later, need to add a field in the MappedExpectedChallengeAnswer table
challenge_type = stringOrNull(challenge.challengeType),
link = link
)
})
}
// catch { case _ : Throwable => ChallengeJSON (id = "", allowed_attempts = 0, challenge_type = "")}
catch { case _ : Throwable => null}

View File

@ -400,6 +400,14 @@ trait Connector extends MdcLoggable {
// Validates an answer for a challenge and returns if the answer is correct or not
def validateChallengeAnswer(challengeId: String, hashOfSuppliedAnswer: String, callContext: Option[CallContext]): OBPReturnType[Box[Boolean]] = Future{(Full(true), callContext)}
def allChallengesSuccessfullyAnswered(
bankId: BankId,
accountId: AccountId,
transReqId: TransactionRequestId,
challenges: List[ChallengeTrait],
callContext: Option[CallContext]
): OBPReturnType[Box[Boolean]]= Future{(Full(true), callContext)}
def validateChallengeAnswerC2(
transactionRequestId: Option[String],
consentId: Option[String],

View File

@ -385,6 +385,27 @@ object LocalMappedConnector extends Connector with MdcLoggable {
(Full(Challenges.ChallengeProvider.vend.validateChallenge(challengeId, hashOfSuppliedAnswer, userId).isDefined), callContext)
}
override def allChallengesSuccessfullyAnswered(
bankId: BankId,
accountId: AccountId,
transReqId: TransactionRequestId,
challenges: List[ChallengeTrait],
callContext: Option[CallContext]
): OBPReturnType[Box[Boolean]] = {
for {
(accountAttributes, callContext) <- Connector.connector.vend.getAccountAttributesByAccount(bankId, accountId, callContext)
quorum = accountAttributes.toList.flatten.find(_.name == "REQUIRED_CHALLENGE_ANSWERS").map(_.value).getOrElse("1").toInt
challengeSuccess = challenges.count(_.successful == true) match {
case number if number >= quorum => true
case _ =>
MappedTransactionRequestProvider.saveTransactionRequestStatusImpl(transReqId, TransactionRequestStatus.NEXT_CHALLENGE_PENDING.toString)
false
}
} yield {
(Full(challengeSuccess), callContext)
}
}
override def getChargeLevel(bankId: BankId,
accountId: AccountId,
@ -4898,19 +4919,22 @@ object LocalMappedConnector extends Connector with MdcLoggable {
users <- getUsersForChallenges(fromAccount.bankId, fromAccount.accountId)
//now we support multiple challenges. We can support multiple people to answer the challenges.
//So here we return the challengeIds.
(challengeIds, callContext) <- Connector.connector.vend.createChallenges(
fromAccount.bankId,
fromAccount.accountId,
users.toList.flatten.map(_.userId),
transactionRequestType: TransactionRequestType,
transactionRequest.id.value,
scaMethod,
callContext
) map { i =>
(challenges, callContext) <- Connector.connector.vend.createChallengesC2(
userIds = users.toList.flatten.map(_.userId),
challengeType = ChallengeType.OBP_PAYMENT,//TODO, here just hard code, it is TransactionChallengeTypes.Value in createTransactionRequestv400 method
transactionRequestId = Some(transactionRequest.id.value),
scaMethod = scaMethod,
scaStatus = None, //Only use for BerlinGroup Now
consentId = None, // Note: consentId and transactionRequestId are exclusive here.
authenticationMethodId = None,
callContext = callContext
) map { i =>
(unboxFullOrFail(i._1, callContext, s"$InvalidConnectorResponseForCreateChallenge ", 400), i._2)
}
//TODO, this challengeIds are only for mapped connector now. we only return the first challengeId in the request yet.
newChallenge = TransactionRequestChallenge(challengeIds.headOption.getOrElse(""), allowed_attempts = 3, challenge_type = challengeType.getOrElse(TransactionChallengeTypes.OTP_VIA_API.toString))
//NOTE:this is only for Backward compatibility, now we use the MappedExpectedChallengeAnswer tables instead of the single field in TransactionRequest.
//Here only put the dummy date.
newChallenge = TransactionRequestChallenge(s"challenges number:${challenges.length}", allowed_attempts = 3, challenge_type = TransactionChallengeTypes.OTP_VIA_API.toString)
_ <- Future(saveTransactionRequestChallenge(transactionRequest.id, newChallenge))
transactionRequest <- Future(transactionRequest.copy(challenge = newChallenge))
} yield {

View File

@ -1,7 +1,6 @@
package code.bankconnectors.akka
import java.util.Date
import akka.pattern.ask
import code.actorsystem.ObpLookupSystem
import code.api.ResourceDocs1_4_0.MessageDocsSwaggerDefinitions
@ -12,6 +11,7 @@ import code.api.util.ExampleValue._
import code.api.util._
import code.bankconnectors._
import code.bankconnectors.akka.actor.{AkkaConnectorActorInit, AkkaConnectorHelperActor}
import code.transactionrequests.TransactionRequests.TransactionChallengeTypes
import com.openbankproject.commons.ExecutionContext.Implicits.global
import com.openbankproject.commons.dto._
import com.openbankproject.commons.model._

View File

@ -56,43 +56,42 @@ object MappedChallengeProvider extends ChallengeProvider {
challengeAnswer: String,
userId: Option[String]
): Box[ChallengeTrait] = {
val challenge = getChallenge(challengeId).openOrThrowException(s"${ErrorMessages.InvalidChallengeAnswer}")
val currentAttemptCounterValue = challenge.attemptCounter
//We update the counter anyway.
challenge.mAttemptCounter(currentAttemptCounterValue+1).saveMe()
val createDateTime = challenge.createdAt.get
val challengeTTL : Long = Helpers.seconds(APIUtil.transactionRequestChallengeTtl)
val expiredDateTime: Long = createDateTime.getTime+challengeTTL
val currentTime: Long = Platform.currentTime
//TODO, add column maxAttemptsAllowed (Int) to `mappedexpectedchallengeanswer` table instead of this hardcode number 3
if(currentAttemptCounterValue <3){
if(expiredDateTime > currentTime) {
val currentHashedAnswer = BCrypt.hashpw(challengeAnswer, challenge.salt).substring(0, 44)
val expectedHashedAnswer = challenge.expectedAnswer
userId match {
case None =>
if(currentHashedAnswer==expectedHashedAnswer) {
tryo{challenge.mSuccessful(true).mScaStatus(StrongCustomerAuthenticationStatus.finalised.toString).saveMe()}
} else {
Failure(s"${ErrorMessages.InvalidChallengeAnswer}")
}
case Some(id) =>
if(currentHashedAnswer==expectedHashedAnswer && id==challenge.expectedUserId) {
tryo{challenge.mSuccessful(true).mScaStatus(StrongCustomerAuthenticationStatus.finalised.toString).saveMe()}
} else {
Failure(s"${ErrorMessages.InvalidChallengeAnswer}")
}
for{
challenge <- getChallenge(challengeId) ?~! s"${ErrorMessages.InvalidTransactionRequestChallengeId}"
currentAttemptCounterValue = challenge.attemptCounter
//We update the counter anyway.
_ = challenge.mAttemptCounter(currentAttemptCounterValue+1).saveMe()
createDateTime = challenge.createdAt.get
challengeTTL : Long = Helpers.seconds(APIUtil.transactionRequestChallengeTtl)
expiredDateTime: Long = createDateTime.getTime+challengeTTL
currentTime: Long = Platform.currentTime
challenge <- if(currentAttemptCounterValue <3){
if(expiredDateTime > currentTime) {
val currentHashedAnswer = BCrypt.hashpw(challengeAnswer, challenge.salt).substring(0, 44)
val expectedHashedAnswer = challenge.expectedAnswer
userId match {
case None =>
if(currentHashedAnswer==expectedHashedAnswer) {
tryo{challenge.mSuccessful(true).mScaStatus(StrongCustomerAuthenticationStatus.finalised.toString).saveMe()}
} else {
Failure(s"${ErrorMessages.InvalidChallengeAnswer}")
}
case Some(id) =>
if(currentHashedAnswer==expectedHashedAnswer && id==challenge.expectedUserId) {
tryo{challenge.mSuccessful(true).mScaStatus(StrongCustomerAuthenticationStatus.finalised.toString).saveMe()}
} else {
Failure(s"${ErrorMessages.InvalidChallengeAnswer}")
}
}
}else{
Failure(s"${ErrorMessages.OneTimePasswordExpired} Current expiration time is $transactionRequestChallengeTtl seconds")
}
}else{
Failure(s"${ErrorMessages.OneTimePasswordExpired} Current expiration time is $transactionRequestChallengeTtl seconds")
Failure(s"${ErrorMessages.AllowedAttemptsUsedUp}")
}
}else{
Failure(s"${ErrorMessages.AllowedAttemptsUsedUp}")
} yield{
challenge
}
}
}