feature/support the IMPLICIT SCA method for createConsent endpoint

This commit is contained in:
hongwei 2023-06-12 22:11:06 +08:00
parent 561ccc6f88
commit 21ca38df20
6 changed files with 236 additions and 10 deletions

View File

@ -4189,6 +4189,15 @@ object SwaggerDefinitionsJSON {
valid_from = Some(new Date()),
time_to_live = Some(3600)
)
val postConsentImplicitJsonV310 = PostConsentImplicitJsonV310(
everything = false,
views = List(PostConsentViewJsonV310(bankIdExample.value, accountIdExample.value, viewIdExample.value)),
entitlements = List(PostConsentEntitlementJsonV310(bankIdExample.value, "CanGetCustomer")),
consumer_id = Some(consumerIdExample.value),
valid_from = Some(new Date()),
time_to_live = Some(3600)
)
val postConsentRequestJsonV310 = postConsentPhoneJsonV310.copy(consumer_id = None)
val consentsJsonV310 = ConsentsJsonV310(List(consentJsonV310))

View File

@ -490,7 +490,7 @@ object ErrorMessages {
val ConsentCheckExpiredIssue = "OBP-35006: Cannot check is Consent-Id expired. "
val ConsentDisabled = "OBP-35007: Consents are not allowed at this instance. "
val ConsentHeaderNotFound = "OBP-35008: Cannot get Consent-Id. "
val ConsentAllowedScaMethods = "OBP-35009: Only SMS and EMAIL are supported as SCA methods. "
val ConsentAllowedScaMethods = "OBP-35009: Only SMS, EMAIL and IMPLICIT are supported as SCA methods. "
val SmsServerNotResponding = "OBP-35010: SMS server is not working or SMS server can not send the message to the phone number:"
val AuthorizationNotFound = "OBP-35011: Resource identification of the related Consent authorisation sub-resource not found by AUTHORIZATION_ID. "
val ConsentAlreadyRevoked = "OBP-35012: Consent is already revoked. "
@ -515,6 +515,7 @@ object ErrorMessages {
val ConsumerKeyIsToLong = "OBP-35031: The Consumer Key max length <= 512"
val ConsentHeaderValueInvalid = "OBP-35032: The Consent's Request Header value is not formatted as UUID or JWT."
val RolesForbiddenInConsent = s"OBP-35033: Consents cannot contain the following Roles: ${canCreateEntitlementAtOneBank} and ${canCreateEntitlementAtAnyBank}."
val UserAuthContextUpdateRequestAllowedScaMethods = "OBP-35034: Only SMS and EMAIL are supported as SCA methods. "
//Authorisations
val AuthorisationNotFound = "OBP-36001: Authorisation not found. Please specify valid values for PAYMENT_ID and AUTHORISATION_ID. "

View File

@ -3235,8 +3235,9 @@ trait APIMethods310 {
|
|The Consent is created in an ${ConsentStatus.INITIATED} state.
|
|A One Time Password (OTP) (AKA security challenge) is sent Out of band (OOB) to the User via the transport defined in SCA_METHOD
|SCA_METHOD is typically "SMS" or "EMAIL". "EMAIL" is used for testing purposes.
|A One Time Password (OTP) (AKA security challenge) is sent Out of Band (OOB) to the User via the transport defined in SCA_METHOD
|SCA_METHOD is typically "SMS","EMAIL" or "IMPLICIT". "EMAIL" is used for testing purposes. OBP mapped mode "IMPLICIT" is "EMAIL".
|Other mode, bank can decide it in the connector method 'getConsentImplicitSCA'.
|
|When the Consent is created, OBP (or a backend system) stores the challenge so it can be checked later against the value supplied by the User with the Answer Consent Challenge endpoint.
|
@ -3250,7 +3251,7 @@ trait APIMethods310 {
| "views": [],
| "entitlements": [],
| "consumer_id": "7uy8a7e4-6d02-40e3-a129-0b2bf89de8uh",
| "email": "eveline@example.com"
| "phone_number": "+49 170 1234567"
|}
|
|Please note that consumer_id is optional field
@ -3259,7 +3260,7 @@ trait APIMethods310 {
| "everything": true,
| "views": [],
| "entitlements": [],
| "email": "eveline@example.com"
| "phone_number": "+49 170 1234567"
|}
|
|Please note if everything=false you need to explicitly specify views and entitlements
@ -3280,7 +3281,7 @@ trait APIMethods310 {
| }
| ],
| "consumer_id": "7uy8a7e4-6d02-40e3-a129-0b2bf89de8uh",
| "email": "eveline@example.com"
| "phone_number": "+49 170 1234567"
|}
|
|""",
@ -3314,7 +3315,8 @@ trait APIMethods310 {
|The Consent is created in an ${ConsentStatus.INITIATED} state.
|
|A One Time Password (OTP) (AKA security challenge) is sent Out of Band (OOB) to the User via the transport defined in SCA_METHOD
|SCA_METHOD is typically "SMS" or "EMAIL". "EMAIL" is used for testing purposes.
|SCA_METHOD is typically "SMS","EMAIL" or "IMPLICIT". "EMAIL" is used for testing purposes. OBP mapped mode "IMPLICIT" is "EMAIL".
|Other mode, bank can decide it in the connector method 'getConsentImplicitSCA'.
|
|When the Consent is created, OBP (or a backend system) stores the challenge so it can be checked later against the value supplied by the User with the Answer Consent Challenge endpoint.
|
@ -3380,8 +3382,87 @@ trait APIMethods310 {
),
apiTagConsent :: apiTagPSD2AIS :: apiTagPsd2 :: Nil)
resourceDocs += ResourceDoc(
createConsentImplicit,
implementedInApiVersion,
nameOf(createConsentImplicit),
"POST",
"/banks/BANK_ID/my/consents/IMPLICIT",
"Create Consent (IMPLICIT)",
s"""
|
|This endpoint starts the process of creating a Consent.
|
|The Consent is created in an ${ConsentStatus.INITIATED} state.
|
|A One Time Password (OTP) (AKA security challenge) is sent Out of Band (OOB) to the User via the transport defined in SCA_METHOD
|SCA_METHOD is typically "SMS","EMAIL" or "IMPLICIT". "EMAIL" is used for testing purposes. OBP mapped mode "IMPLICIT" is "EMAIL".
|Other mode, bank can decide it in the connector method 'getConsentImplicitSCA'.
|
|When the Consent is created, OBP (or a backend system) stores the challenge so it can be checked later against the value supplied by the User with the Answer Consent Challenge endpoint.
|
|$generalObpConsentText
|
|${authenticationRequiredMessage(true)}
|
|Example 1:
|{
| "everything": true,
| "views": [],
| "entitlements": [],
| "consumer_id": "7uy8a7e4-6d02-40e3-a129-0b2bf89de8uh",
|}
|
|Please note that consumer_id is optional field
|Example 2:
|{
| "everything": true,
| "views": [],
| "entitlements": [],
|}
|
|Please note if everything=false you need to explicitly specify views and entitlements
|Example 3:
|{
| "everything": false,
| "views": [
| {
| "bank_id": "GENODEM1GLS",
| "account_id": "8ca8a7e4-6d02-40e3-a129-0b2bf89de9f0",
| "view_id": "owner"
| }
| ],
| "entitlements": [
| {
| "bank_id": "GENODEM1GLS",
| "role_name": "CanGetCustomer"
| }
| ],
| "consumer_id": "7uy8a7e4-6d02-40e3-a129-0b2bf89de8uh",
|}
|
|""",
postConsentImplicitJsonV310,
consentJsonV310,
List(
UserNotLoggedIn,
BankNotFound,
InvalidJsonFormat,
ConsentAllowedScaMethods,
RolesAllowedInConsent,
ViewsAllowedInConsent,
ConsumerNotFoundByConsumerId,
ConsumerIsDisabled,
MissingPropsValueAtThisInstance,
SmsServerNotResponding,
InvalidConnectorResponse,
UnknownError
),
apiTagConsent :: apiTagPSD2AIS :: apiTagPsd2 :: Nil)
lazy val createConsentEmail = createConsent
lazy val createConsentSms = createConsent
lazy val createConsentImplicit = createConsent
lazy val createConsent : OBPEndpoint = {
case "banks" :: BankId(bankId) :: "my" :: "consents" :: scaMethod :: Nil JsonPost json -> _ => {
@ -3390,7 +3471,7 @@ trait APIMethods310 {
(Full(user), callContext) <- authenticatedAccess(cc)
(_, callContext) <- NewStyle.function.getBank(bankId, callContext)
_ <- Helper.booleanToFuture(ConsentAllowedScaMethods, cc=callContext){
List(StrongCustomerAuthentication.SMS.toString(), StrongCustomerAuthentication.EMAIL.toString()).exists(_ == scaMethod)
List(StrongCustomerAuthentication.SMS.toString(), StrongCustomerAuthentication.EMAIL.toString(), StrongCustomerAuthentication.IMPLICIT.toString()).exists(_ == scaMethod)
}
failMsg = s"$InvalidJsonFormat The Json body should be the $PostConsentBodyCommonJson "
consentJson <- NewStyle.function.tryons(failMsg, 400, callContext) {
@ -3485,6 +3566,32 @@ trait APIMethods310 {
callContext
)
} yield Future{status}
case v if v == StrongCustomerAuthentication.IMPLICIT.toString => // Not implemented
for {
(consentImplicitSCA, callContext) <- NewStyle.function.getConsentImplicitSCA(user, callContext)
status <- consentImplicitSCA.scaMethod match {
case v if v == StrongCustomerAuthentication.EMAIL => // Send the email
Connector.connector.vend.sendCustomerNotification (
StrongCustomerAuthentication.EMAIL,
consentImplicitSCA.recipient,
Some ("OBP Consent Challenge"),
challengeText,
callContext
)
case v if v == StrongCustomerAuthentication.SMS => // Not implemented
Connector.connector.vend.sendCustomerNotification(
StrongCustomerAuthentication.SMS,
consentImplicitSCA.recipient,
None,
challengeText,
callContext
)
case _ => Future {
"Success"
}
}} yield {
status
}
case _ =>Future{"Success"}
}
} yield {
@ -3671,7 +3778,7 @@ trait APIMethods310 {
checkScope(bankId.value, getConsumerPrimaryKey(callContext), ApiRole.canCreateUserAuthContextUpdate)
}
(_, callContext) <- NewStyle.function.getBank(bankId, callContext)
_ <- Helper.booleanToFuture(ConsentAllowedScaMethods, cc=callContext){
_ <- Helper.booleanToFuture(UserAuthContextUpdateRequestAllowedScaMethods, cc=callContext){
List(StrongCustomerAuthentication.SMS.toString(), StrongCustomerAuthentication.EMAIL.toString()).exists(_ == scaMethod)
}
failMsg = s"$InvalidJsonFormat The Json body should be the $PostUserAuthContextJson "

View File

@ -556,6 +556,15 @@ case class PostConsentPhoneJsonV310(
time_to_live: Option[Long]
) extends PostConsentCommonBody
case class PostConsentImplicitJsonV310(
everything: Boolean,
views: List[PostConsentViewJsonV310],
entitlements: List[PostConsentEntitlementJsonV310],
consumer_id: Option[String],
valid_from: Option[Date],
time_to_live: Option[Long]
) extends PostConsentCommonBody
case class ConsentJsonV310(consent_id: String, jwt: String, status: String)
case class ConsentsJsonV310(consents: List[ConsentJsonV310])

View File

@ -512,7 +512,7 @@ trait APIMethods500 {
_ <- Helper.booleanToFuture(failMsg = ConsumerHasMissingRoles + CanCreateUserAuthContextUpdate, cc=callContext) {
checkScope(bankId.value, getConsumerPrimaryKey(callContext), ApiRole.canCreateUserAuthContextUpdate)
}
_ <- Helper.booleanToFuture(ConsentAllowedScaMethods, cc=callContext){
_ <- Helper.booleanToFuture(UserAuthContextUpdateRequestAllowedScaMethods, cc=callContext){
List(StrongCustomerAuthentication.SMS.toString(), StrongCustomerAuthentication.EMAIL.toString()).exists(_ == scaMethod)
}
failMsg = s"$InvalidJsonFormat The Json body should be the $PostUserAuthContextJson "

View File

@ -64,6 +64,10 @@ class ConsentTest extends V310ServerSetup {
.copy(entitlements=entitlements)
.copy(consumer_id=Some(testConsumer.consumerId.get))
.copy(views=views)
lazy val postConsentImplicitJsonV310 = SwaggerDefinitionsJSON.postConsentImplicitJsonV310
.copy(entitlements=entitlements)
.copy(consumer_id=Some(testConsumer.consumerId.get))
.copy(views=views)
val maxTimeToLive = APIUtil.getPropsAsIntValue(nameOfProperty="consents.max_time_to_live", defaultValue=3600)
val timeToLive: Option[Long] = Some(maxTimeToLive + 10)
@ -78,6 +82,15 @@ class ConsentTest extends V310ServerSetup {
response400.code should equal(401)
response400.body.extract[ErrorMessage].message should equal(UserNotLoggedIn)
}
scenario("We will call the endpoint without user credentials-IMPLICIT", ApiEndpoint1, VersionOfApi) {
When("We make a request")
val request400 = (v3_1_0_Request / "banks" / bankId / "my" / "consents" / "IMPLICIT" ).POST
val response400 = makePostRequest(request400, write(postConsentImplicitJsonV310))
Then("We should get a 401")
response400.code should equal(401)
response400.body.extract[ErrorMessage].message should equal(UserNotLoggedIn)
}
scenario("We will call the endpoint with user credentials but wrong SCA method", ApiEndpoint1, VersionOfApi) {
When("We make a request")
@ -95,6 +108,14 @@ class ConsentTest extends V310ServerSetup {
scenario("We will call the endpoint with user credentials and deprecated header name", ApiEndpoint1, ApiEndpoint3, VersionOfApi, VersionOfApi2) {
wholeFunctionality(RequestHeader.`Consent-Id`)
}
scenario("We will call the endpoint with user credentials-Implicit", ApiEndpoint1, ApiEndpoint3, VersionOfApi, VersionOfApi2) {
wholeFunctionalityImplicit(RequestHeader.`Consent-JWT`)
}
scenario("We will call the endpoint with user credentials and deprecated header name-Implicit", ApiEndpoint1, ApiEndpoint3, VersionOfApi, VersionOfApi2) {
wholeFunctionalityImplicit(RequestHeader.`Consent-Id`)
}
}
private def wholeFunctionality(nameOfRequestHeader: String) = {
@ -175,4 +196,83 @@ class ConsentTest extends V310ServerSetup {
responseGetUserByUserId400.body.extract[ErrorMessage].message should include(ConsentDisabled)
}
}
private def wholeFunctionalityImplicit(nameOfRequestHeader: String) = {
When("We make a request")
// Create a consent as the user1.
// Must fail because we try to set time_to_live=4500
val requestWrongTimeToLive400 = (v3_1_0_Request / "banks" / bankId / "my" / "consents" / "IMPLICIT").POST <@ (user1)
val responseWrongTimeToLive400 = makePostRequest(requestWrongTimeToLive400, write(postConsentImplicitJsonV310.copy(time_to_live = timeToLive)))
Then("We should get a 400")
responseWrongTimeToLive400.code should equal(400)
responseWrongTimeToLive400.body.extract[ErrorMessage].message should include(ConsentMaxTTL)
// Create a consent as the user1.
// Must fail because we try to assign a role other that user already have access to the request
val request400 = (v3_1_0_Request / "banks" / bankId / "my" / "consents" / "IMPLICIT").POST <@ (user1)
val response400 = makePostRequest(request400, write(postConsentImplicitJsonV310))
Then("We should get a 400")
response400.code should equal(400)
response400.body.extract[ErrorMessage].message should equal(RolesAllowedInConsent)
Then("We grant the role and test it again")
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetAnyUser.toString)
// Create a consent as the user1. The consent is in status INITIATED
val secondResponse400 = makePostRequest(request400, write(postConsentImplicitJsonV310))
Then("We should get a 201")
secondResponse400.code should equal(201)
val consentId = secondResponse400.body.extract[ConsentJsonV310].consent_id
val jwt = secondResponse400.body.extract[ConsentJsonV310].jwt
val header = List((nameOfRequestHeader, jwt))
// Make a request with the consent which is NOT in status ACCEPTED
val requestGetUserByUserId400 = (v3_1_0_Request / "users" / "current").GET
val responseGetUserByUserId400 = makeGetRequest(requestGetUserByUserId400, header)
APIUtil.getPropsAsBoolValue(nameOfProperty = "consents.allowed", defaultValue = false) match {
case true =>
// Due to the wrong status of the consent the request must fail
responseGetUserByUserId400.body.extract[ErrorMessage].message should include(ConsentStatusIssue)
// Answer security challenge i.e. SCA
val answerConsentChallengeRequest = (v3_1_0_Request / "banks" / bankId / "consents" / consentId / "challenge").POST <@ (user1)
val challenge = Consent.challengeAnswerAtTestEnvironment
val post = PostConsentChallengeJsonV310(answer = challenge)
val response400 = makePostRequest(answerConsentChallengeRequest, write(post))
Then("We should get a 201")
response400.code should equal(201)
// Make a request WITHOUT the request header "Consumer-Key: SOME_VALUE"
// Due to missing value the request must fail
makeGetRequest(requestGetUserByUserId400, header)
.body.extract[ErrorMessage].message should include(ConsumerKeyHeaderMissing)
// Make a request WITH the request header "Consumer-Key: NON_EXISTING_VALUE"
// Due to non existing value the request must fail
val headerConsumerKey = List((RequestHeader.`Consumer-Key`, "NON_EXISTING_VALUE"))
makeGetRequest(requestGetUserByUserId400, header ::: headerConsumerKey)
.body.extract[ErrorMessage].message should include(ConsentDoesNotMatchConsumer)
// Make a request WITH the request header "Consumer-Key: EXISTING_VALUE"
val validHeaderConsumerKey = List((RequestHeader.`Consumer-Key`, user1.map(_._1.key).getOrElse("SHOULD_NOT_HAPPEN")))
val response = makeGetRequest((v3_1_0_Request / "users" / "current").GET, header ::: validHeaderConsumerKey)
val user = response.body.extract[UserJsonV300]
val assignedEntitlements: Seq[PostConsentEntitlementJsonV310] = user.entitlements.list.flatMap(
e => entitlements.find(_ == PostConsentEntitlementJsonV310(e.bank_id, e.role_name))
)
// Check we have all entitlements from the consent
assignedEntitlements should equal(entitlements)
// Every consent implies a brand new user is created
user.user_id should not equal (resourceUser1.userId)
// Check we have all views from the consent
val assignedViews = user.views.map(_.list).toSeq.flatten
assignedViews.map(e => PostConsentViewJsonV310(e.bank_id, e.account_id, e.view_id)).distinct should equal(views)
case false =>
// Due to missing props at the instance the request must fail
responseGetUserByUserId400.body.extract[ErrorMessage].message should include(ConsentDisabled)
}
}
}