Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions obp-api/src/main/resources/props/sample.props.template
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ jwt.use.ssl=false
## Public certificate for the CA (used by clients and servers to validate signatures)
# truststore.path.tpp_signature = path/to/ca.p12
# truststore.password.tpp_signature = truststore-password
# truststore.alias.tpp_signature = alias-name

# Bypass TPP signature validation
# bypass_tpp_signature_validation = false
Expand Down Expand Up @@ -939,8 +940,9 @@ database_messages_scheduler_interval=3600
#

# -- PSD2 Certificates --------------------------
# In case isn't defined default value is "false"
# requirePsd2Certificates=false
# Possible cases: ONLINE, CERTIFICATE, NONE
# In case isn't defined default value is "NONE"
# requirePsd2Certificates=NONE
# -----------------------------------------------

# -- OBP-API mode -------------------------------
Expand Down
4 changes: 4 additions & 0 deletions obp-api/src/main/scala/code/api/constant/constant.scala
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ object CertificateConstants {
final val BEGIN_CERT: String = "-----BEGIN CERTIFICATE-----"
final val END_CERT: String = "-----END CERTIFICATE-----"
}
object PrivateKeyConstants {
final val BEGIN_KEY: String = "-----BEGIN PRIVATE KEY-----"
final val END_KEY: String = "-----END PRIVATE KEY-----"
}

object JedisMethod extends Enumeration {
type JedisMethod = Value
Expand Down
57 changes: 30 additions & 27 deletions obp-api/src/main/scala/code/api/util/APIUtil.scala
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import code.api.oauth1a.OauthParams._
import code.api.util.APIUtil.ResourceDoc.{findPathVariableNames, isPathVariable}
import code.api.util.ApiRole._
import code.api.util.ApiTag.{ResourceDocTag, apiTagBank}
import code.api.util.BerlinGroupSigning.getCertificateFromTppSignatureCertificate
import code.api.util.FutureUtil.{EndpointContext, EndpointTimeout}
import code.api.util.Glossary.GlossaryItem
import code.api.v1_2.ErrorMessage
Expand Down Expand Up @@ -75,7 +76,6 @@ import com.openbankproject.commons.model._
import com.openbankproject.commons.model.enums.StrongCustomerAuthentication.SCA
import com.openbankproject.commons.model.enums.{ContentParam, PemCertificateRole, StrongCustomerAuthentication}
import com.openbankproject.commons.util.Functions.Implicits._
import com.openbankproject.commons.util.Functions.Memo
import com.openbankproject.commons.util._
import dispatch.url
import javassist.expr.{ExprEditor, MethodCall}
Expand Down Expand Up @@ -106,8 +106,8 @@ import java.util.regex.Pattern
import java.util.{Calendar, Date, UUID}
import scala.collection.JavaConverters._
import scala.collection.immutable.{List, Nil}
import scala.collection.mutable
import scala.collection.mutable.{ArrayBuffer, ListBuffer}
import scala.collection.{immutable, mutable}
import scala.concurrent.Future
import scala.io.BufferedSource
import scala.util.control.Breaks.{break, breakable}
Expand Down Expand Up @@ -3238,15 +3238,15 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
* @param cc The call context of an request
* @return Failure in case we exceeded rate limit
*/
def anonymousAccess(cc: CallContext): Future[(Box[User], Option[CallContext])] = {
def anonymousAccess(cc: CallContext): OBPReturnType[Box[User]] = {
getUserAndSessionContextFuture(cc) map { result =>
val url = result._2.map(_.url).getOrElse("None")
val verb = result._2.map(_.verb).getOrElse("None")
val body = result._2.flatMap(_.httpBody)
val reqHeaders = result._2.map(_.requestHeaders).getOrElse(Nil)
// Verify signed request
JwsUtil.verifySignedRequest(body, verb, url, reqHeaders, result)
} map { result =>
} flatMap { result =>
val url = result._2.map(_.url).getOrElse("None")
val verb = result._2.map(_.verb).getOrElse("None")
val body = result._2.flatMap(_.httpBody)
Expand Down Expand Up @@ -3302,7 +3302,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
val reqHeaders = result._2.map(_.requestHeaders).getOrElse(Nil)
// Verify signed request if need be
JwsUtil.verifySignedRequest(body, verb, url, reqHeaders, result)
} map { result =>
} flatMap { result =>
val url = result._2.map(_.url).getOrElse("None")
val verb = result._2.map(_.verb).getOrElse("None")
val body = result._2.flatMap(_.httpBody)
Expand Down Expand Up @@ -3925,9 +3925,26 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
}

private def passesPsd2ServiceProviderCommon(cc: Option[CallContext], serviceProvider: String) = {
val result: Box[Boolean] = getPropsAsBoolValue("requirePsd2Certificates", false) match {
case false => Full(true)
case true =>
val result = getPropsValue("requirePsd2Certificates", "NONE") match {
case value if value.toUpperCase == "ONLINE" =>
val requestHeaders = cc.map(_.requestHeaders).getOrElse(Nil)
val consumerName = cc.flatMap(_.consumer.map(_.name.get)).getOrElse("")
val certificate = getCertificateFromTppSignatureCertificate(requestHeaders)
for {
tpp <- BerlinGroupSigning.getTppByCertificate(certificate, cc)
} yield {
if (tpp.nonEmpty) {
val hasRole = tpp.exists(_.services.contains(serviceProvider))
if (hasRole) {
Full(true)
} else {
Failure(X509ActionIsNotAllowed)
}
} else {
Failure("No valid Tpp")
}
}
case value if value.toUpperCase == "CERTIFICATE" => Future {
`getPSD2-CERT`(cc.map(_.requestHeaders).getOrElse(Nil)) match {
case Some(pem) =>
logger.debug("PSD2-CERT pem: " + pem)
Expand All @@ -3947,14 +3964,17 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
}
case None => Failure(X509CannotGetCertificate)
}
}
case _ =>
Future(Full(true))
}
result
}

def passesPsd2ServiceProvider(cc: Option[CallContext], serviceProvider: String): OBPReturnType[Box[Boolean]] = {
val result = passesPsd2ServiceProviderCommon(cc, serviceProvider)
Future(result) map {
x => (fullBoxOrException(x ~> APIFailureNewStyle(X509GeneralError, 400, cc.map(_.toLight))), cc)
result map {
x => (fullBoxOrException(x ~> APIFailureNewStyle(X509GeneralError, 401, cc.map(_.toLight))), cc)
}
}
def passesPsd2Aisp(cc: Option[CallContext]): OBPReturnType[Box[Boolean]] = {
Expand All @@ -3971,23 +3991,6 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
}


def passesPsd2ServiceProviderOldStyle(cc: Option[CallContext], serviceProvider: String): Box[Boolean] = {
passesPsd2ServiceProviderCommon(cc, serviceProvider) ?~! X509GeneralError
}
def passesPsd2AispOldStyle(cc: Option[CallContext]): Box[Boolean] = {
passesPsd2ServiceProviderOldStyle(cc, PemCertificateRole.PSP_AI.toString())
}
def passesPsd2PispOldStyle(cc: Option[CallContext]): Box[Boolean] = {
passesPsd2ServiceProviderOldStyle(cc, PemCertificateRole.PSP_PI.toString())
}
def passesPsd2IcspOldStyle(cc: Option[CallContext]): Box[Boolean] = {
passesPsd2ServiceProviderOldStyle(cc, PemCertificateRole.PSP_IC.toString())
}
def passesPsd2AsspOldStyle(cc: Option[CallContext]): Box[Boolean] = {
passesPsd2ServiceProviderOldStyle(cc, PemCertificateRole.PSP_AS.toString())
}



def getMaskedPrimaryAccountNumber(accountNumber: String): String = {
val (first, second) = accountNumber.splitAt(accountNumber.size/2)
Expand Down
23 changes: 17 additions & 6 deletions obp-api/src/main/scala/code/api/util/BerlinGroupCheck.scala
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package code.api.util

import code.api.APIFailureNewStyle
import code.api.util.APIUtil.fullBoxOrException
import code.api.util.APIUtil.{OBPReturnType, fullBoxOrException}
import code.util.Helper.MdcLoggable
import com.openbankproject.commons.model.User
import com.openbankproject.commons.util.ApiVersion
import net.liftweb.common.{Box, Empty}
import net.liftweb.http.provider.HTTPParam

object BerlinGroupCheck {
import scala.concurrent.Future
import com.openbankproject.commons.ExecutionContext.Implicits.global

object BerlinGroupCheck extends MdcLoggable {


private val defaultMandatoryHeaders = "Content-Type,Date,Digest,PSU-Device-ID,PSU-Device-Name,PSU-IP-Address,Signature,TPP-Signature-Certificate,X-Request-ID"
Expand Down Expand Up @@ -35,17 +39,24 @@ object BerlinGroupCheck {
}
}

def validate(body: Box[String], verb: String, url: String, reqHeaders: List[HTTPParam], forwardResult: (Box[User], Option[CallContext])): (Box[User], Option[CallContext]) = {
def validate(body: Box[String], verb: String, url: String, reqHeaders: List[HTTPParam], forwardResult: (Box[User], Option[CallContext])): OBPReturnType[Box[User]] = {
if(url.contains(ApiVersion.berlinGroupV13.urlPrefix)) {
validateHeaders(verb, url, reqHeaders, forwardResult) match {
case (user, _) if user.isDefined || user == Empty => // All good. Chain another check
// Verify signed request (Berlin Group)
BerlinGroupSigning.verifySignedRequest(body, verb, url, reqHeaders, forwardResult)
BerlinGroupSigning.verifySignedRequest(body, verb, url, reqHeaders, forwardResult) match {
case (user, cc) if (user.isDefined || user == Empty) && cc.exists(_.consumer.isEmpty) => // There is no Consumer in the database
// Create Consumer on the fly on a first usage of RequestHeader.`TPP-Signature-Certificate`
logger.info(s"Start BerlinGroupSigning.getOrCreateConsumer")
BerlinGroupSigning.getOrCreateConsumer(reqHeaders, forwardResult)
case forwardError => // Forward error case
Future(forwardError)
}
case forwardError => // Forward error case
forwardError
Future(forwardError)
}
} else {
forwardResult
Future(forwardResult)
}
}

Expand Down
2 changes: 2 additions & 0 deletions obp-api/src/main/scala/code/api/util/BerlinGroupError.scala
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ object BerlinGroupError {

case "403" if message.contains("OBP-35001") => "CONSENT_UNKNOWN"

case "401" if message.contains("OBP-20300") => "CERTIFICATE_BLOCKED"
case "401" if message.contains("OBP-20312") => "CERTIFICATE_INVALID"
case "401" if message.contains("OBP-20300") => "CERTIFICATE_INVALID"
case "401" if message.contains("OBP-20310") => "SIGNATURE_INVALID"

case "401" if message.contains("OBP-20060") => "ROLE_INVALID"
Expand Down
Loading