mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 17:56:46 +00:00
Tweak Consent. Use Consent-JWT instead of Consent-Id
This commit is contained in:
parent
d4bc4cb0e7
commit
bee4e9d8c3
@ -267,8 +267,8 @@ trait OBPRestHelper extends RestHelper with MdcLoggable {
|
||||
)
|
||||
if(newStyleEndpoints(rd)) {
|
||||
fn(cc)
|
||||
} else if (APIUtil.hasConsentId(reqHeaders)) {
|
||||
val (usr, callContext) = Consent.applyRulesOldStyle(APIUtil.getConsentId(reqHeaders), cc)
|
||||
} else if (APIUtil.hasConsentJWT(reqHeaders)) {
|
||||
val (usr, callContext) = Consent.applyRulesOldStyle(APIUtil.getConsentJWT(reqHeaders), cc)
|
||||
usr match {
|
||||
case Full(u) => fn(callContext.copy(user = Full(u))) // Authentication is successful
|
||||
case ParamFailure(a, b, c, apiFailure : APIFailure) => ParamFailure(a, b, c, apiFailure : APIFailure)
|
||||
|
||||
@ -37,7 +37,9 @@ object ChargePolicy extends Enumeration {
|
||||
|
||||
object RequestHeader {
|
||||
final lazy val `Consumer-Key` = "Consumer-Key"
|
||||
@deprecated("Use Consent-JWT","11-03-2020")
|
||||
final lazy val `Consent-Id` = "Consent-Id"
|
||||
final lazy val `Consent-JWT` = "Consent-JWT"
|
||||
final lazy val `PSD2-CERT` = "PSD2-CERT"
|
||||
}
|
||||
object ResponseHeader {
|
||||
|
||||
@ -169,13 +169,16 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
|
||||
}
|
||||
|
||||
/**
|
||||
* Purpose of this helper function is to get the Consent-Id value from a Request Headers.
|
||||
* @return the Consent-Id value from a Request Header as a String
|
||||
* Purpose of this helper function is to get the Consent-JWT value from a Request Headers.
|
||||
* @return the Consent-JWT value from a Request Header as a String
|
||||
*/
|
||||
def getConsentId(requestHeaders: List[HTTPParam]): Option[String] = {
|
||||
requestHeaders.toSet.filter(_.name == RequestHeader.`Consent-Id`).toList match {
|
||||
def getConsentJWT(requestHeaders: List[HTTPParam]): Option[String] = {
|
||||
requestHeaders.toSet.filter(_.name == RequestHeader.`Consent-JWT`).toList match {
|
||||
case x :: Nil => Some(x.values.mkString(", "))
|
||||
case _ => None
|
||||
case _ => requestHeaders.toSet.filter(_.name == RequestHeader.`Consent-Id`).toList match {
|
||||
case x :: Nil => Some(x.values.mkString(", "))
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
@ -188,8 +191,8 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
def hasConsentId(requestHeaders: List[HTTPParam]): Boolean = {
|
||||
getConsentId(requestHeaders).isDefined
|
||||
def hasConsentJWT(requestHeaders: List[HTTPParam]): Boolean = {
|
||||
getConsentJWT(requestHeaders).isDefined
|
||||
}
|
||||
|
||||
def registeredApplication(consumerKey: String): Boolean = {
|
||||
@ -2222,8 +2225,8 @@ Returns a string showed to the developer
|
||||
val reqHeaders = S.request.openOrThrowException(attemptedToOpenAnEmptyBox).request.headers
|
||||
val remoteIpAddress = getRemoteIpAddress()
|
||||
val res =
|
||||
if (APIUtil.hasConsentId(reqHeaders)) {
|
||||
Consent.applyRules(APIUtil.getConsentId(reqHeaders), cc)
|
||||
if (APIUtil.hasConsentJWT(reqHeaders)) {
|
||||
Consent.applyRules(APIUtil.getConsentJWT(reqHeaders), cc)
|
||||
} else if (hasAnOAuthHeader(cc.authReqHeaderField)) {
|
||||
getUserFromOAuthHeaderFuture(cc)
|
||||
} else if (hasAnOAuth2Header(cc.authReqHeaderField)) {
|
||||
|
||||
@ -122,14 +122,14 @@ case class CallContext(
|
||||
)
|
||||
}
|
||||
/**
|
||||
* Purpose of this helper function is to get the Consent-Id value from a Request Headers.
|
||||
* @return the Consent-Id value from a Request Header as a String
|
||||
* Purpose of this helper function is to get the Consent-JWT value from a Request Headers.
|
||||
* @return the Consent-JWT value from a Request Header as a String
|
||||
*/
|
||||
def getConsentId(): Option[String] = {
|
||||
APIUtil.getConsentId(this.requestHeaders)
|
||||
APIUtil.getConsentJWT(this.requestHeaders)
|
||||
}
|
||||
def hasConsentId(): Boolean = {
|
||||
APIUtil.hasConsentId(this.requestHeaders)
|
||||
APIUtil.hasConsentJWT(this.requestHeaders)
|
||||
}
|
||||
|
||||
// for endpoint body convenient get userId
|
||||
|
||||
@ -97,7 +97,7 @@ object Consent {
|
||||
|
||||
/**
|
||||
* Purpose of this helper function is to get the Consumer-Key value from a Request Headers.
|
||||
* @return the Consent-Id value from a Request Header as a String
|
||||
* @return the Consumer-Key value from a Request Header as a String
|
||||
*/
|
||||
def getConsumerKey(requestHeaders: List[HTTPParam]): Option[String] = {
|
||||
requestHeaders.toSet.filter(_.name == RequestHeader.`Consumer-Key`).toList match {
|
||||
@ -262,7 +262,7 @@ object Consent {
|
||||
case Full(jsonAsString) =>
|
||||
try {
|
||||
val consent = net.liftweb.json.parse(jsonAsString).extract[ConsentJWT]
|
||||
checkConsent(consent, consentIdAsJwt, calContext) match { // Check is it Consent-Id expired
|
||||
checkConsent(consent, consentIdAsJwt, calContext) match { // Check is it Consent-JWT expired
|
||||
case (Full(true)) => // OK
|
||||
applyConsentRules(consent)
|
||||
case failure@Failure(_, _, _) => // Handled errors
|
||||
@ -306,7 +306,7 @@ object Consent {
|
||||
case Full(jsonAsString) =>
|
||||
try {
|
||||
val consent = net.liftweb.json.parse(jsonAsString).extract[ConsentJWT]
|
||||
checkConsent(consent, consentIdAsJwt, calContext) match { // Check is it Consent-Id expired
|
||||
checkConsent(consent, consentIdAsJwt, calContext) match { // Check is it Consent-JWT expired
|
||||
case (Full(true)) => // OK
|
||||
applyConsentRules(consent)
|
||||
case failure@Failure(_, _, _) => // Handled errors
|
||||
|
||||
@ -3331,15 +3331,15 @@ trait APIMethods310 {
|
||||
|
|
||||
|Each Consent has one of the following states: ${ConsentStatus.values.toList.sorted.mkString(", ") }.
|
||||
|
|
||||
|Each Consent is bound to an consumer i.e. you need to identify yourself over request header value Consumer-Key.
|
||||
|Each Consent is bound to a consumer i.e. you need to identify yourself over request header value Consumer-Key.
|
||||
|For example:
|
||||
|GET /obp/v4.0.0/users/current HTTP/1.1
|
||||
|Host: 127.0.0.1:8080
|
||||
|Consent-Id: eyJhbGciOiJIUzI1NiJ9.eyJlbnRpdGxlbWVudHMiOlt7InJvbGVfbmFtZSI6IkNhbkdldEFueVVzZXIiLCJiYW5rX2lkIjoiIn1dLCJjcmVhdGVkQnlVc2VySWQiOiJhYjY1MzlhOS1iMTA1LTQ0ODktYTg4My0wYWQ4ZDZjNjE2NTciLCJzdWIiOiIzNDc1MDEzZi03YmY5LTQyNjEtOWUxYy0xZTdlNWZjZTJlN2UiLCJhdWQiOiI4MTVhMGVmMS00YjZhLTQyMDUtYjExMi1lNDVmZDZmNGQzYWQiLCJuYmYiOjE1ODA3NDE2NjcsImlzcyI6Imh0dHA6XC9cLzEyNy4wLjAuMTo4MDgwIiwiZXhwIjoxNTgwNzQ1MjY3LCJpYXQiOjE1ODA3NDE2NjcsImp0aSI6ImJkYzVjZTk5LTE2ZTYtNDM4Yi1hNjllLTU3MTAzN2RhMTg3OCIsInZpZXdzIjpbXX0.L3fEEEhdCVr3qnmyRKBBUaIQ7dk1VjiFaEBW8hUNjfg
|
||||
|Consent-JWT: eyJhbGciOiJIUzI1NiJ9.eyJlbnRpdGxlbWVudHMiOlt7InJvbGVfbmFtZSI6IkNhbkdldEFueVVzZXIiLCJiYW5rX2lkIjoiIn1dLCJjcmVhdGVkQnlVc2VySWQiOiJhYjY1MzlhOS1iMTA1LTQ0ODktYTg4My0wYWQ4ZDZjNjE2NTciLCJzdWIiOiIzNDc1MDEzZi03YmY5LTQyNjEtOWUxYy0xZTdlNWZjZTJlN2UiLCJhdWQiOiI4MTVhMGVmMS00YjZhLTQyMDUtYjExMi1lNDVmZDZmNGQzYWQiLCJuYmYiOjE1ODA3NDE2NjcsImlzcyI6Imh0dHA6XC9cLzEyNy4wLjAuMTo4MDgwIiwiZXhwIjoxNTgwNzQ1MjY3LCJpYXQiOjE1ODA3NDE2NjcsImp0aSI6ImJkYzVjZTk5LTE2ZTYtNDM4Yi1hNjllLTU3MTAzN2RhMTg3OCIsInZpZXdzIjpbXX0.L3fEEEhdCVr3qnmyRKBBUaIQ7dk1VjiFaEBW8hUNjfg
|
||||
|Consumer-Key: ejznk505d132ryomnhbx1qmtohurbsbb0kijajsk
|
||||
|cache-control: no-cache
|
||||
|
|
||||
|Maximum time to live of te token is specified over props value consents.max_time_to_live. In case isn't defined default value is 3600 seconds.
|
||||
|Maximum time to live of the token is specified over props value consents.max_time_to_live. In case isn't defined default value is 3600 seconds.
|
||||
|
|
||||
|Example of POST JSON:
|
||||
|{
|
||||
@ -3362,7 +3362,7 @@ trait APIMethods310 {
|
||||
| "valid_from": "2020-02-07T08:43:34Z",
|
||||
| "time_to_live": 3600
|
||||
|}
|
||||
|Please ote that only optional fields are: consumer_id, valid_from and time_to_live.
|
||||
|Please note that only optional fields are: consumer_id, valid_from and time_to_live.
|
||||
|In case you omit they the default values are used:
|
||||
|consumer_id = consumer of current user
|
||||
|valid_from = current time
|
||||
|
||||
@ -87,84 +87,92 @@ class ConsentTest extends V310ServerSetup {
|
||||
response400.code should equal(400)
|
||||
response400.body.extract[ErrorMessage].message should equal(ConsentAllowedScaMethods)
|
||||
}
|
||||
|
||||
|
||||
scenario("We will call the endpoint with user credentials", ApiEndpoint1, ApiEndpoint3, VersionOfApi, VersionOfApi2) {
|
||||
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" / "EMAIL" ).POST <@(user1)
|
||||
val responseWrongTimeToLive400 = makePostRequest(requestWrongTimeToLive400, write(postConsentEmailJsonV310.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" / "EMAIL" ).POST <@(user1)
|
||||
val response400 = makePostRequest(request400, write(postConsentEmailJsonV310))
|
||||
Then("We should get a 400")
|
||||
response400.code should equal(400)
|
||||
response400.body.extract[ErrorMessage].message should equal(RolesAllowedInConsent)
|
||||
wholeFunctionality(RequestHeader.`Consent-JWT`)
|
||||
}
|
||||
|
||||
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(postConsentEmailJsonV310))
|
||||
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((RequestHeader.`Consent-Id`, 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)
|
||||
scenario("We will call the endpoint with user credentials and deprecated header name", ApiEndpoint1, ApiEndpoint3, VersionOfApi, VersionOfApi2) {
|
||||
wholeFunctionality(RequestHeader.`Consent-Id`)
|
||||
}
|
||||
}
|
||||
|
||||
// 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(ConsentDoesntMatchApp)
|
||||
|
||||
// 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 user = makeGetRequest((v3_1_0_Request / "users" / "current").GET, header ::: validHeaderConsumerKey)
|
||||
.body.extract[UserJsonV300]
|
||||
val assignedEntitlements: Seq[EntitlementJsonV400] = user.entitlements.list.flatMap(
|
||||
e => entitlements.find(_ == EntitlementJsonV400(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)
|
||||
private def wholeFunctionality(name: 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" / "EMAIL").POST <@ (user1)
|
||||
val responseWrongTimeToLive400 = makePostRequest(requestWrongTimeToLive400, write(postConsentEmailJsonV310.copy(time_to_live = timeToLive)))
|
||||
Then("We should get a 400")
|
||||
responseWrongTimeToLive400.code should equal(400)
|
||||
responseWrongTimeToLive400.body.extract[ErrorMessage].message should include(ConsentMaxTTL)
|
||||
|
||||
// Check we have all views from the consent
|
||||
val assignedViews = user.views.map(_.list).toSeq.flatten
|
||||
assignedViews.map(e => ViewJsonV400(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)
|
||||
}
|
||||
// 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" / "EMAIL").POST <@ (user1)
|
||||
val response400 = makePostRequest(request400, write(postConsentEmailJsonV310))
|
||||
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(postConsentEmailJsonV310))
|
||||
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((RequestHeader.`Consent-Id`, 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(ConsentDoesntMatchApp)
|
||||
|
||||
// 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 user = makeGetRequest((v3_1_0_Request / "users" / "current").GET, header ::: validHeaderConsumerKey)
|
||||
.body.extract[UserJsonV300]
|
||||
val assignedEntitlements: Seq[EntitlementJsonV400] = user.entitlements.list.flatMap(
|
||||
e => entitlements.find(_ == EntitlementJsonV400(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 => ViewJsonV400(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