mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 12:56:51 +00:00
feature/support the IMPLICIT SCA method for createConsent endpoint
This commit is contained in:
parent
561ccc6f88
commit
21ca38df20
@ -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))
|
||||
|
||||
@ -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. "
|
||||
|
||||
@ -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 "
|
||||
|
||||
@ -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])
|
||||
|
||||
|
||||
@ -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 "
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user