From da6bd064c87dbd3be73d219f68589b4084d5844d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Mon, 4 Dec 2023 11:35:37 +0100 Subject: [PATCH 1/5] docfix/Tweak endpoint updateConsumerRedirectUrl v5.1.0 --- .../src/main/scala/code/api/v5_1_0/APIMethods510.scala | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 41545ead0..438562cc6 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 @@ -1769,6 +1769,14 @@ trait APIMethods510 { } + private def consumerDisabledText() = { + if(APIUtil.getPropsAsBoolValue("consumers_enabled_by_default", false) == false) { + "Please note: Your consumer may be disabled as a result of this action." + } else { + "" + } + } + staticResourceDocs += ResourceDoc( updateConsumerRedirectUrl, implementedInApiVersion, @@ -1777,6 +1785,8 @@ trait APIMethods510 { "/management/consumers/CONSUMER_ID/consumer/redirect_url", "Update Consumer RedirectUrl", s"""Update an existing redirectUrl for a Consumer specified by CONSUMER_ID. + | + | ${consumerDisabledText()} | | CONSUMER_ID can be obtained after you register the application. | From 6f8f40e5e6f598ce05c4efdf5e12797258597ba1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 5 Dec 2023 13:03:10 +0100 Subject: [PATCH 2/5] feature/Dynamic Registration of Consumer - WIP --- .../SwaggerDefinitionsJSON.scala | 33 ++++++--- .../scala/code/api/util/ErrorMessages.scala | 1 + .../scala/code/api/v5_1_0/APIMethods510.scala | 69 +++++++++++++++++++ .../code/api/v5_1_0/JSONFactory5.1.0.scala | 22 ++++-- 4 files changed, 112 insertions(+), 13 deletions(-) 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 0aaa6dfa3..a91347362 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 @@ -2667,6 +2667,30 @@ object SwaggerDefinitionsJSON { enabled = true, created = DateWithDayExampleObject ) + lazy val pem = "-----BEGIN CERTIFICATE-----\nMIIFIjCCBAqgAwIBAgIIX3qsz7QQxngwDQYJKoZIhvcNAQELBQAwgZ8xCzAJBgNV\r\nBAYTAkRFMQ8wDQYDVQQIEwZCZXJsaW4xDzANBgNVBAcTBkJlcmxpbjEPMA0GA1UE\r\nChMGVEVTT0JFMRowGAYDVQQLExFURVNPQkUgT3BlcmF0aW9uczESMBAGA1UEAxMJ\r\nVEVTT0JFIENBMR8wHQYJKoZIhvcNAQkBFhBhZG1pbkB0ZXNvYmUuY29tMQwwCgYD\r\nVQQpEwNWUE4wHhcNMjMwNzE3MDg0MDAwWhcNMjQwNzE3MDg0MDAwWjCBizELMAkG\r\nA1UEBhMCREUxDzANBgNVBAgTBkJlcmxpbjEPMA0GA1UEBxMGQmVybGluMRQwEgYD\r\nVQQKEwtUZXNvYmUgR21iSDEPMA0GA1UECxMGc3lzb3BzMRIwEAYDVQQDEwlsb2Nh\r\nbGhvc3QxHzAdBgkqhkiG9w0BCQEWEGFkbWluQHRlc29iZS5jb20wggEiMA0GCSqG\r\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCwxGuWUN1H0d0IeYPYWdLA0I/5BXx4DLO6\r\nzfi1GGJlF8BIXRN0VTJckIY9C3J1RnXDs6p6ufA01iHe1PQdL6VzfcaC3j+jUSgV\r\n1z9ybEUPyUwq3PCCxqoVI9n8yh+O6FDn3dvu/9Q2NtBpJHUBDCLf7OO9TgsFU2sE\r\nMys+Hw5DuuX5n5OQ2VIwH+qlMTQnd+yw5y8FKHqAZT5hE60lF/x6sQnwi58hLGRW\r\nSqo/548c2ZpoeWtnyY1I6PyR7zUYGuhruLY8gVFfLE+610u/lj2wYTXMxntpV+tV\r\nralLFRMhvbqZXW/EpuDb/pEbCnLDNDxq5NarLVDzcHs7VhT9MPChAgMBAAGjggFy\r\nMIIBbjATBgNVHSUEDDAKBggrBgEFBQcDAjAaBgNVHREEEzARgglsb2NhbGhvc3SH\r\nBH8AAAEwggEGBggrBgEFBQcBAwSB+TCB9jAIBgYEAI5GAQEwOAYGBACORgEFMC4w\r\nLBYhaHR0cHM6Ly9leGFtcGxlLm9yZy9wa2lkaXNjbG9zdXJlEwdleGFtcGxlMIGI\r\nBgYEAIGYJwIwfjBMMBEGBwQAgZgnAQMMBlBTUF9BSTARBgcEAIGYJwEBDAZQU1Bf\r\nQVMwEQYHBACBmCcBAgwGUFNQX1BJMBEGBwQAgZgnAQQMBlBTUF9JQwwlRHVtbXkg\r\nRmluYW5jaWFsIFN1cGVydmlzaW9uIEF1dGhvcml0eQwHWFgtREZTQTAlBgYEAI5G\r\nAQYwGwYHBACORgEGAQYHBACORgEGAgYHBACORgEGAzARBglghkgBhvhCAQEEBAMC\r\nB4AwHgYJYIZIAYb4QgENBBEWD3hjYSBjZXJ0aWZpY2F0ZTANBgkqhkiG9w0BAQsF\r\nAAOCAQEAKTS7exS9A7rWJLRzWrlHoTu68Avm5g9Dz1GKjgt8rnvj3D21SE14Rf5p\r\n0JWHYH4SiCdnh8Tx+IA7o0TmPJ1JRfAXR3i/5R7TJi/HrnqL+V7SIx2Cuq/hkZEU\r\nAhVs07nnvHURcrlQGwcfn4TbgpCURpCPpYZlNsYySb6BS6I4qFaadHGqMTyEkphV\r\nwfXyB3brmzxj9V4Qgp0t+s/uFuFirWyIayRc9nSSC7vuNVYvib2Kim4y8kvuWpA4\r\nZ51+fFOmBqCqpmwfAADNgDsLJiA/741eBflVd/ZUeAzgOjMCMIaDGlwiwZlePKT7\r\n553GtfsGxZMf05oqfUrQEQfJaU+/+Q==\n-----END CERTIFICATE-----\n" + lazy val certificateInfoJsonV510 = CertificateInfoJsonV510( + subject_domain_name = "OID.2.5.4.41=VPN, EMAILADDRESS=admin@tesobe.com, CN=TESOBE CA, OU=TESOBE Operations, O=TESOBE, L=Berlin, ST=Berlin, C=DE", + issuer_domain_name = "CN=localhost, O=TESOBE GmbH, ST=Berlin, C=DE", + not_before = "2022-04-01T10:13:00.000Z", + not_after = "2032-04-01T10:13:00.000Z", + roles = None, + roles_info = Some("PEM Encoded Certificate does not contain PSD2 roles.") + ) + lazy val consumerJsonV510: ConsumerJsonV510 = ConsumerJsonV510( + consumer_id = "d0d7b08c-f0ec-4e57-ac99-7d9eafe99225", + consumer_key = "d0d7b08c-f0ec-4e57-ac99-7d9eafe99225", + consumer_secret = "d0d7b08c-f0ec-4e57-ac99-7d9eafe99225", + app_name = "SOFI", + app_type = "Web", + description = "Account Management", + developer_email = ExampleValue.emailExample.value, + redirect_url = "www.openbankproject.com", + certificate_pem = pem, + certificate_info = Some(certificateInfoJsonV510), + created_by_user = resourceUserJSON, + enabled = true, + created = DateWithDayExampleObject + ) val consumersJson = ConsumersJson( list = List(consumerJSON) @@ -4200,15 +4224,6 @@ object SwaggerDefinitionsJSON { val oAuth2ServerJWKURIJson = OAuth2ServerJWKURIJson("https://www.googleapis.com/oauth2/v3/certs") val oAuth2ServerJwksUrisJson = OAuth2ServerJwksUrisJson(List(oAuth2ServerJWKURIJson)) - - val certificateInfoJsonV510 = CertificateInfoJsonV510( - subject_domain_name = "OID.2.5.4.41=VPN, EMAILADDRESS=admin@tesobe.com, CN=TESOBE CA, OU=TESOBE Operations, O=TESOBE, L=Berlin, ST=Berlin, C=DE", - issuer_domain_name = "CN=localhost, O=TESOBE GmbH, ST=Berlin, C=DE", - not_before = "2022-04-01T10:13:00.000Z", - not_after = "2032-04-01T10:13:00.000Z", - roles = None, - roles_info = Some("PEM Encoded Certificate does not contain PSD2 roles.") - ) val updateAccountRequestJsonV310 = UpdateAccountRequestJsonV310( label = "Label", 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 611e437e9..934207029 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -519,6 +519,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." // Consents val ConsentNotFound = "OBP-35001: Consent not found by CONSENT_ID. " 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 438562cc6..0630947f8 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 @@ -10,6 +10,7 @@ import code.api.util.ErrorMessages.{$UserNotLoggedIn, BankNotFound, ConsentNotFo import code.api.util.FutureUtil.{EndpointContext, EndpointTimeout} import code.api.util.NewStyle.HttpCode import code.api.util._ +import code.api.util.newstyle.Consumer.createConsumerNewStyle import code.api.util.newstyle.RegulatedEntityNewStyle.{createRegulatedEntityNewStyle, deleteRegulatedEntityNewStyle, getRegulatedEntitiesNewStyle, getRegulatedEntityByEntityIdNewStyle} import code.api.v2_1_0.{ConsumerRedirectUrlJSON, JSONFactory210} import code.api.v3_0_0.JSONFactory300 @@ -23,6 +24,7 @@ import code.bankconnectors.Connector import code.consent.Consents import code.loginattempts.LoginAttempt import code.metrics.APIMetrics +import code.model.AppType import code.model.dataAccess.MappedBankAccount import code.regulatedentities.MappedRegulatedEntityProvider import code.transactionrequests.TransactionRequests.TransactionRequestTypes.{apply => _} @@ -41,6 +43,7 @@ import net.liftweb.common.Full import net.liftweb.http.rest.RestHelper import net.liftweb.json.parse import net.liftweb.mapper.By +import net.liftweb.util.Helpers import net.liftweb.util.Helpers.tryo import scala.collection.immutable.{List, Nil} @@ -1769,6 +1772,72 @@ trait APIMethods510 { } + staticResourceDocs += ResourceDoc( + createConsumer, + implementedInApiVersion, + "createConsumer", + "POST", + "/dynamic-registration/consumers", + "Create a Consumer", + s"""Create a Consumer (mTLS access). + | + |""", + ConsumerPostJsonV510( + "TESOBE GmbH", + "Test", + "Web", + "Description", + "some@email.com", + "redirecturl", + ), + consumerJsonV510, + List( + InvalidJsonFormat, + UnknownError + ), + List(apiTagConsumer), + Some(Nil)) + + + lazy val createConsumer: OBPEndpoint = { + case "dynamic-registration" :: "consumers" :: Nil JsonPost json -> _ => { + cc => + implicit val ec = EndpointContext(Some(cc)) + for { + postedJson <- NewStyle.function.tryons(InvalidJsonFormat, 400, cc.callContext) { + json.extract[ConsumerPostJsonV510] + } + pem = APIUtil.`getPSD2-CERT`(cc.requestHeaders) + certificateInfo: CertificateInfoJsonV510 <- Future(X509.getCertificateInfo(pem)) map { + unboxFullOrFail(_, cc.callContext, X509GeneralError) + } + _ <- Helper.booleanToFuture(RegulatedEntityNotFoundByCertificate, 400, cc.callContext) { + MappedRegulatedEntityProvider.getRegulatedEntities() + .exists(_.entityCertificatePublicKey.replace("""\n""", "") == pem.getOrElse("").replace("""\n""", "")) + } + (consumer, callContext) <- createConsumerNewStyle( + key = Some(Helpers.randomString(40).toLowerCase), + secret = Some(Helpers.randomString(40).toLowerCase), + isActive = Some(true), + name = Some(postedJson.app_name), + appType = Some(AppType.valueOf(postedJson.app_type)), + description = Some(postedJson.description), + developerEmail = Some(postedJson.developer_email), + redirectURL = Some(postedJson.redirect_url), + createdByUserId = None, + clientCertificate = pem, + cc.callContext + ) + } yield { + // Format the data as json + val json = JSONFactory510.createConsumerJSON(consumer, Some(certificateInfo)) + // Return + (json, HttpCode.`201`(callContext)) + } + } + } + + private def consumerDisabledText() = { if(APIUtil.getPropsAsBoolValue("consumers_enabled_by_default", false) == false) { "Please note: Your consumer may be disabled as a result of this action." 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 ccd8eb5d8..9ec054fed 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,13 +281,23 @@ case class MetricJsonV510( ) case class MetricsJsonV510(metrics: List[MetricJsonV510]) +case class ConsumerPostJsonV510(entity_name: String, + app_name: String, + app_type: String, + description: String, + developer_email: String, + redirect_url: String, + ) case class ConsumerJsonV510(consumer_id: String, + consumer_key: String, + consumer_secret: String, app_name: String, app_type: String, description: String, developer_email: String, redirect_url: String, - created_by_user_id: String, + certificate_pem: String, + certificate_info: Option[CertificateInfoJsonV510], created_by_user: ResourceUserJSON, enabled: Boolean, created: Date @@ -620,7 +630,7 @@ object JSONFactory510 extends CustomJsonFormats { MetricsJsonV510(metrics.map(createMetricJson)) } - def createConsumerJSON(c: Consumer): ConsumerJsonV510 = { + def createConsumerJSON(c: Consumer, certificateInfo: Option[CertificateInfoJsonV510] = None): ConsumerJsonV510 = { val resourceUserJSON = Users.users.vend.getUserByUserId(c.createdByUserId.toString()) match { case Full(resourceUser) => ResourceUserJSON( @@ -633,13 +643,17 @@ object JSONFactory510 extends CustomJsonFormats { case _ => null } - ConsumerJsonV510(consumer_id = c.consumerId.get, + ConsumerJsonV510( + consumer_id = c.consumerId.get, + consumer_key = c.key.get, + consumer_secret = c.secret.get, app_name = c.name.get, app_type = c.appType.toString(), description = c.description.get, developer_email = c.developerEmail.get, redirect_url = c.redirectURL.get, - created_by_user_id = c.createdByUserId.get, + certificate_pem = c.clientCertificate.get, + certificate_info = certificateInfo, created_by_user = resourceUserJSON, enabled = c.isActive.get, created = c.createdAt.get From 35b7c5755e4bc73d0cbc3930051eba335026898e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 6 Dec 2023 11:41:49 +0100 Subject: [PATCH 3/5] feature/Dynamic Registration of Consumer - WIP 2 --- .../src/main/scala/code/api/util/X509.scala | 36 +++++++++++++++++-- .../code/api/util/newstyle/Consumer.scala | 11 +++--- .../scala/code/api/v5_1_0/APIMethods510.scala | 17 ++++----- .../code/api/v5_1_0/JSONFactory5.1.0.scala | 8 ++--- 4 files changed, 53 insertions(+), 19 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/X509.scala b/obp-api/src/main/scala/code/api/util/X509.scala index 1d2d6d71c..051353e07 100644 --- a/obp-api/src/main/scala/code/api/util/X509.scala +++ b/obp-api/src/main/scala/code/api/util/X509.scala @@ -10,7 +10,7 @@ import code.util.Helper.MdcLoggable import com.github.dwickern.macros.NameOf import com.nimbusds.jose.jwk.RSAKey import com.nimbusds.jose.util.X509CertUtils -import net.liftweb.common.{Box, Failure, Full} +import net.liftweb.common.{Box, Failure, Full, Empty} import org.bouncycastle.asn1._ import org.bouncycastle.asn1.x509.Extension import org.bouncycastle.asn1.x509.qualified.QCStatement @@ -246,5 +246,37 @@ object X509 extends MdcLoggable { case None => Failure(ErrorMessages.X509CannotGetCertificate) } } - + + def getCommonName(pem: Option[String]): Box[String] = { + getFieldCommon(pem, "CN") + } + def getOrganization(pem: Option[String]): Box[String] = { + getFieldCommon(pem, "O") + } + def getOrganizationUnit(pem: Option[String]): Box[String] = { + getFieldCommon(pem, "OU") + } + def getEmailAddress(pem: Option[String]): Box[String] = { + getFieldCommon(pem, "EMAILADDRESS") + .or(getFieldCommon(pem, "EMAILADDRESS".toLowerCase())) + } + + private def getFieldCommon(pem: Option[String], field: String) = { + pem match { + case Some(unboxedPem) => + extractCertificateInfo(unboxedPem).map { item => + val splitByComma: Array[String] = item.subject_domain_name.split(",") + val splitByKeyValuePair: Array[(String, String)] = splitByComma.map(i => i.split("=")(0).trim -> i.split("=")(1).trim) + val valuesAsMap: Map[String, List[String]] = splitByKeyValuePair.toList.groupBy(_._1).map { case (k, v) => (k, v.map(_._2)) } + val result: String = valuesAsMap.get(field).map(_.mkString).getOrElse("") + result + } match { + case Full(value) if value.isEmpty => Empty + case everythingElse => everythingElse + } + case _ => + Empty + } + } + } diff --git a/obp-api/src/main/scala/code/api/util/newstyle/Consumer.scala b/obp-api/src/main/scala/code/api/util/newstyle/Consumer.scala index 9625229a4..251ac8051 100644 --- a/obp-api/src/main/scala/code/api/util/newstyle/Consumer.scala +++ b/obp-api/src/main/scala/code/api/util/newstyle/Consumer.scala @@ -1,7 +1,8 @@ package code.api.util.newstyle -import code.api.util.APIUtil.{OBPReturnType, unboxFull} +import code.api.util.APIUtil.{OBPReturnType, unboxFull, unboxFullOrFail} import code.api.util.CallContext +import code.api.util.ErrorMessages.CreateConsumerError import code.consumer.Consumers import code.model.{AppType, Consumer} @@ -34,11 +35,11 @@ object Consumer { redirectURL, createdByUserId, clientCertificate - ) map { - (_, callContext) - } + ) } map { - unboxFull(_) + (_, callContext) + } map { + x => (unboxFullOrFail(x._1, callContext, CreateConsumerError, 400), x._2) } } 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 0630947f8..ecfe6f6c5 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 @@ -9,6 +9,7 @@ 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.NewStyle.HttpCode +import code.api.util.X509.{getCommonName, getEmailAddress} import code.api.util._ import code.api.util.newstyle.Consumer.createConsumerNewStyle import code.api.util.newstyle.RegulatedEntityNewStyle.{createRegulatedEntityNewStyle, deleteRegulatedEntityNewStyle, getRegulatedEntitiesNewStyle, getRegulatedEntityByEntityIdNewStyle} @@ -1784,11 +1785,11 @@ trait APIMethods510 { |""", ConsumerPostJsonV510( "TESOBE GmbH", - "Test", - "Web", + Some("Test"), + Some("Web"), "Description", - "some@email.com", - "redirecturl", + Some("some@email.com"), + Some("redirecturl"), ), consumerJsonV510, List( @@ -1819,11 +1820,11 @@ trait APIMethods510 { key = Some(Helpers.randomString(40).toLowerCase), secret = Some(Helpers.randomString(40).toLowerCase), isActive = Some(true), - name = Some(postedJson.app_name), - appType = Some(AppType.valueOf(postedJson.app_type)), + name = getCommonName(pem).or(postedJson.app_name) , + appType = postedJson.app_type.map(AppType.valueOf).orElse(Some(AppType.valueOf("Confidential"))), description = Some(postedJson.description), - developerEmail = Some(postedJson.developer_email), - redirectURL = Some(postedJson.redirect_url), + developerEmail = getEmailAddress(pem).or(postedJson.developer_email), + redirectURL = postedJson.redirect_url, createdByUserId = None, clientCertificate = pem, cc.callContext 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 9ec054fed..0046fda95 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 @@ -282,11 +282,11 @@ case class MetricJsonV510( case class MetricsJsonV510(metrics: List[MetricJsonV510]) case class ConsumerPostJsonV510(entity_name: String, - app_name: String, - app_type: String, + app_name: Option[String], + app_type: Option[String], description: String, - developer_email: String, - redirect_url: String, + developer_email: Option[String], + redirect_url: Option[String], ) case class ConsumerJsonV510(consumer_id: String, consumer_key: String, From e12fecd8ed8ccd5a4d4a045734f98e9b2e29b155 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 6 Dec 2023 13:48:03 +0100 Subject: [PATCH 4/5] feature/Dynamic Registration of Consumer - WIP 3 --- .../ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala | 1 + .../main/scala/code/api/util/newstyle/Consumer.scala | 4 +++- .../main/scala/code/api/v4_0_0/APIMethods400.scala | 1 + .../main/scala/code/api/v5_1_0/APIMethods510.scala | 12 ++++++------ .../scala/code/api/v5_1_0/JSONFactory5.1.0.scala | 5 +++-- 5 files changed, 14 insertions(+), 9 deletions(-) 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 a91347362..5914c12b4 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 @@ -2684,6 +2684,7 @@ object SwaggerDefinitionsJSON { app_type = "Web", description = "Account Management", developer_email = ExampleValue.emailExample.value, + company = ExampleValue.companyExample.value, redirect_url = "www.openbankproject.com", certificate_pem = pem, certificate_info = Some(certificateInfoJsonV510), diff --git a/obp-api/src/main/scala/code/api/util/newstyle/Consumer.scala b/obp-api/src/main/scala/code/api/util/newstyle/Consumer.scala index 251ac8051..e71cab7db 100644 --- a/obp-api/src/main/scala/code/api/util/newstyle/Consumer.scala +++ b/obp-api/src/main/scala/code/api/util/newstyle/Consumer.scala @@ -19,6 +19,7 @@ object Consumer { appType: Option[AppType], description: Option[String], developerEmail: Option[String], + company: Option[String], redirectURL: Option[String], createdByUserId: Option[String], clientCertificate: Option[String], @@ -34,7 +35,8 @@ object Consumer { developerEmail, redirectURL, createdByUserId, - clientCertificate + clientCertificate, + company ) } map { (_, callContext) diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index 0f812e84a..855044d64 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -5295,6 +5295,7 @@ trait APIMethods400 extends MdcLoggable { appType = None, description = Some(postedJson.description), developerEmail = Some(postedJson.developer_email), + company = None, redirectURL = Some(postedJson.redirect_url), createdByUserId = Some(u.userId), clientCertificate = Some(postedJson.clientCertificate), 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 ecfe6f6c5..b5c0c418f 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 @@ -9,7 +9,7 @@ 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.NewStyle.HttpCode -import code.api.util.X509.{getCommonName, getEmailAddress} +import code.api.util.X509.{getCommonName, getEmailAddress, getOrganization} import code.api.util._ import code.api.util.newstyle.Consumer.createConsumerNewStyle import code.api.util.newstyle.RegulatedEntityNewStyle.{createRegulatedEntityNewStyle, deleteRegulatedEntityNewStyle, getRegulatedEntitiesNewStyle, getRegulatedEntityByEntityIdNewStyle} @@ -1784,12 +1784,11 @@ trait APIMethods510 { | |""", ConsumerPostJsonV510( - "TESOBE GmbH", - Some("Test"), - Some("Web"), + None, + None, "Description", - Some("some@email.com"), - Some("redirecturl"), + None, + None, ), consumerJsonV510, List( @@ -1824,6 +1823,7 @@ trait APIMethods510 { appType = postedJson.app_type.map(AppType.valueOf).orElse(Some(AppType.valueOf("Confidential"))), description = Some(postedJson.description), developerEmail = getEmailAddress(pem).or(postedJson.developer_email), + company = getOrganization(pem), redirectURL = postedJson.redirect_url, createdByUserId = None, clientCertificate = pem, 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 0046fda95..cad6d562f 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,8 +281,7 @@ case class MetricJsonV510( ) case class MetricsJsonV510(metrics: List[MetricJsonV510]) -case class ConsumerPostJsonV510(entity_name: String, - app_name: Option[String], +case class ConsumerPostJsonV510(app_name: Option[String], app_type: Option[String], description: String, developer_email: Option[String], @@ -295,6 +294,7 @@ case class ConsumerJsonV510(consumer_id: String, app_type: String, description: String, developer_email: String, + company: String, redirect_url: String, certificate_pem: String, certificate_info: Option[CertificateInfoJsonV510], @@ -651,6 +651,7 @@ object JSONFactory510 extends CustomJsonFormats { app_type = c.appType.toString(), description = c.description.get, developer_email = c.developerEmail.get, + company = c.company.get, redirect_url = c.redirectURL.get, certificate_pem = c.clientCertificate.get, certificate_info = certificateInfo, From 34a8954edcd79e33e80b87f92765c9d703ed82dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 6 Dec 2023 17:56:30 +0100 Subject: [PATCH 5/5] feature/Dynamic Registration of Consumer - WIP 4 --- .../scala/code/api/util/ErrorMessages.scala | 1 + .../main/scala/code/api/util/JwtUtil.scala | 9 +++++ .../scala/code/api/v5_1_0/APIMethods510.scala | 36 +++++++++++++------ .../code/api/v5_1_0/JSONFactory5.1.0.scala | 2 ++ 4 files changed, 37 insertions(+), 11 deletions(-) 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,