Tweak Consent. Use Consent-JWT instead of Consent-Id

This commit is contained in:
Marko Milić 2020-03-11 13:11:31 +01:00
parent d4bc4cb0e7
commit bee4e9d8c3
7 changed files with 109 additions and 96 deletions

View File

@ -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)

View File

@ -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 {

View File

@ -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)) {

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)
}
}
}