diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index 49c1f1947..4742c74a2 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala @@ -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)) diff --git a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala index 490927cb6..1326ac1bf 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -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. " 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 0a3aa24ff..4f261b888 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 @@ -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 " diff --git a/obp-api/src/main/scala/code/api/v3_1_0/JSONFactory3.1.0.scala b/obp-api/src/main/scala/code/api/v3_1_0/JSONFactory3.1.0.scala index f4ceeda39..c7036700a 100644 --- a/obp-api/src/main/scala/code/api/v3_1_0/JSONFactory3.1.0.scala +++ b/obp-api/src/main/scala/code/api/v3_1_0/JSONFactory3.1.0.scala @@ -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]) 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 14a1e64a5..2649a2018 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 @@ -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 " diff --git a/obp-api/src/test/scala/code/api/v3_1_0/ConsentTest.scala b/obp-api/src/test/scala/code/api/v3_1_0/ConsentTest.scala index f9ebfa20e..ca2de8bb7 100644 --- a/obp-api/src/test/scala/code/api/v3_1_0/ConsentTest.scala +++ b/obp-api/src/test/scala/code/api/v3_1_0/ConsentTest.scala @@ -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) + } + } }