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 934207029..429cddc2c 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -520,6 +520,7 @@ object ErrorMessages { val RegulatedEntityNotFound = "OBP-34100: Regulated Entity not found. Please specify a valid value for REGULATED_ENTITY_ID." val RegulatedEntityNotDeleted = "OBP-34101: Regulated Entity cannot be deleted. Please specify a valid value for REGULATED_ENTITY_ID." val RegulatedEntityNotFoundByCertificate = "OBP-34102: Regulated Entity cannot be found by provided certificate." + val PostJsonIsNotSigned = "OBP-34110: JWT at the post json cannot be verified." // Consents val ConsentNotFound = "OBP-35001: Consent not found by CONSENT_ID. " diff --git a/obp-api/src/main/scala/code/api/util/JwtUtil.scala b/obp-api/src/main/scala/code/api/util/JwtUtil.scala index f77b05087..96ec3c33b 100644 --- a/obp-api/src/main/scala/code/api/util/JwtUtil.scala +++ b/obp-api/src/main/scala/code/api/util/JwtUtil.scala @@ -269,6 +269,15 @@ object JwtUtil extends MdcLoggable { jwk.toPublicJWK.toRSAKey } + def verifyJwt(jwtString: String, pemEncodedRsaPublicKey: String): Boolean = { + // Parse PEM-encoded key to RSA public / private JWK + val jwk: JWK = JWK.parseFromPEMEncodedObjects(pemEncodedRsaPublicKey); + val rsaPublicKey: RSAKey = jwk.toPublicJWK.toRSAKey + val signedJWT = SignedJWT.parse(jwtString) + val verifier = new RSASSAVerifier(rsaPublicKey) + signedJWT.verify(verifier) + } + def main(args: Array[String]): Unit = { val jwtToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjhhYWQ2NmJkZWZjMWI0M2Q4ZGIyN2U2NWUyZTJlZjMwMTg3OWQzZTgiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI0MDc0MDg3MTgxOTIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI0MDc0MDg3MTgxOTIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTM5NjY4NTQyNDU3ODA4OTI5NTkiLCJhdF9oYXNoIjoiWGlpckZ1cnJ2X0ZxN3RHd25rLWt1QSIsIm5hbWUiOiJNYXJrbyBNaWxpxIciLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDUuZ29vZ2xldXNlcmNvbnRlbnQuY29tLy1YZDQ0aG5KNlREby9BQUFBQUFBQUFBSS9BQUFBQUFBQUFBQS9BS3hyd2NhZHd6aG00TjR0V2s1RThBdnhpLVpLNmtzNHFnL3M5Ni1jL3Bob3RvLmpwZyIsImdpdmVuX25hbWUiOiJNYXJrbyIsImZhbWlseV9uYW1lIjoiTWlsacSHIiwibG9jYWxlIjoiZW4iLCJpYXQiOjE1NDczMTE3NjAsImV4cCI6MTU0NzMxNTM2MH0.UyOmM0rsO0-G_ibDH3DFogS94GcsNd9GtYVw7j3vSMjO1rZdIraV-N2HUtQN3yHopwdf35A2FEJaag6X8dbvEkJC7_GAynyLIpodoaHNtaLbww6XQSYuQYyF27aPMpROoGZUYkMpB_82LF3PbD4ecDPC2IA5oSyDF4Eya4yn-MzxYmXS7usVWvanREg8iNQSxpu7zZqj4UwhvSIv7wH0vskr_M-PnefQzNTrdUx74i-v9lVqC4E_bF5jWeDGO8k5dqWqg55QuZdyJdSh89KNiIjJXGZDWUBzGfsbetWRnObIgX264fuOW4SpRglUc8fzv41Sc7SSqjqRAFm05t60kg" diff --git a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala index b5c0c418f..18b1e75a2 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala @@ -8,6 +8,7 @@ import code.api.util.ApiRole._ import code.api.util.ApiTag._ import code.api.util.ErrorMessages.{$UserNotLoggedIn, BankNotFound, ConsentNotFound, InvalidJsonFormat, UnknownError, UserNotFoundByUserId, UserNotLoggedIn, _} import code.api.util.FutureUtil.{EndpointContext, EndpointTimeout} +import code.api.util.JwtUtil.{getSignedPayloadAsJson, verifyJwt} import code.api.util.NewStyle.HttpCode import code.api.util.X509.{getCommonName, getEmailAddress, getOrganization} import code.api.util._ @@ -42,7 +43,7 @@ import com.openbankproject.commons.model._ import com.openbankproject.commons.util.{ApiVersion, ScannedApiVersion} import net.liftweb.common.Full import net.liftweb.http.rest.RestHelper -import net.liftweb.json.parse +import net.liftweb.json.{compactRender, parse} import net.liftweb.mapper.By import net.liftweb.util.Helpers import net.liftweb.util.Helpers.tryo @@ -1781,21 +1782,28 @@ trait APIMethods510 { "/dynamic-registration/consumers", "Create a Consumer", s"""Create a Consumer (mTLS access). + | + | JWT payload: + | - minimal + | { "description":"Description" } + | - full + | { + | "description": "Description", + | "app_name": "Tesobe GmbH", + | "app_type": "Sofit", + | "developer_email": "marko@tesobe.com", + | "redirect_url": "http://localhost:8082" + | } + | Please note that JWT must be signed with the counterpart private kew of the public key used to establish mTLS | |""", - ConsumerPostJsonV510( - None, - None, - "Description", - None, - None, - ), + ConsumerJwtPostJsonV510("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkZXNjcmlwdGlvbiI6IkRlc2NyaXB0aW9uIn0.qDnzk1dGK8akdLFRl8fmJV_SeoDjRTDG_eMogCIzZ7M"), consumerJsonV510, List( InvalidJsonFormat, UnknownError ), - List(apiTagConsumer), + List(apiTagDirectory, apiTagConsumer), Some(Nil)) @@ -1804,10 +1812,16 @@ trait APIMethods510 { cc => implicit val ec = EndpointContext(Some(cc)) for { - postedJson <- NewStyle.function.tryons(InvalidJsonFormat, 400, cc.callContext) { - json.extract[ConsumerPostJsonV510] + postedJwt <- NewStyle.function.tryons(InvalidJsonFormat, 400, cc.callContext) { + json.extract[ConsumerJwtPostJsonV510] } pem = APIUtil.`getPSD2-CERT`(cc.requestHeaders) + _ <- Helper.booleanToFuture(PostJsonIsNotSigned, 400, cc.callContext) { + verifyJwt(postedJwt.jwt, pem.getOrElse("")) + } + postedJson <- NewStyle.function.tryons(InvalidJsonFormat, 400, cc.callContext) { + parse(getSignedPayloadAsJson(postedJwt.jwt).getOrElse("{}")).extract[ConsumerPostJsonV510] + } certificateInfo: CertificateInfoJsonV510 <- Future(X509.getCertificateInfo(pem)) map { unboxFullOrFail(_, cc.callContext, X509GeneralError) } diff --git a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala index cad6d562f..6ff9d348e 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala @@ -281,6 +281,8 @@ case class MetricJsonV510( ) case class MetricsJsonV510(metrics: List[MetricJsonV510]) + +case class ConsumerJwtPostJsonV510(jwt: String) case class ConsumerPostJsonV510(app_name: Option[String], app_type: Option[String], description: String,