From 6a7a76b44f8c663648f3551d821b268101c4fcb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 17 Dec 2025 14:51:00 +0100 Subject: [PATCH 1/3] feature/Add OpenAPI 3.1 YAML response --- obp-api/pom.xml | 6 + .../ResourceDocs1_4_0/ResourceDocs140.scala | 89 ++++++++++++++- .../ResourceDocsAPIMethods.scala | 29 ++++- .../main/scala/code/api/util/YAMLUtils.scala | 107 ++++++++++++++++++ 4 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 obp-api/src/main/scala/code/api/util/YAMLUtils.scala diff --git a/obp-api/pom.xml b/obp-api/pom.xml index 0ba0e454a6..c5439fde45 100644 --- a/obp-api/pom.xml +++ b/obp-api/pom.xml @@ -509,6 +509,12 @@ com.fasterxml.jackson.core jackson-databind 2.12.7.1 + + + + tools.jackson.dataformat + jackson-dataformat-yaml + 3.0.3 diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocs140.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocs140.scala index 78b00a8937..3845c33ea5 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocs140.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocs140.scala @@ -1,8 +1,16 @@ package code.api.ResourceDocs1_4_0 +import code.api.Constant.HostName import code.api.OBPRestHelper -import code.util.Helper.MdcLoggable +import code.api.cache.Caching +import code.api.util.APIUtil._ +import code.api.util.{APIUtil, ApiVersionUtils, YAMLUtils} +import code.api.v1_4_0.JSONFactory1_4_0 +import code.apicollectionendpoint.MappedApiCollectionEndpointsProvider +import code.util.Helper.{MdcLoggable, SILENCE_IS_GOLDEN} +import com.openbankproject.commons.model.enums.ContentParam.{DYNAMIC, STATIC} import com.openbankproject.commons.util.{ApiVersion, ApiVersionStatus} +import net.liftweb.http.{GetRequest, InMemoryResponse, PlainTextResponse, Req, S} object ResourceDocs140 extends OBPRestHelper with ResourceDocsAPIMethods with MdcLoggable { @@ -152,6 +160,85 @@ object ResourceDocs300 extends OBPRestHelper with ResourceDocsAPIMethods with Md route }) }) + + // Register YAML endpoint using standard RestHelper approach + serve { + case Req("obp" :: versionStr :: "resource-docs" :: requestedApiVersionString :: "openapi.yaml" :: Nil, _, GetRequest) if versionStr == version.toString => + val (resourceDocTags, partialFunctions, locale, contentParam, apiCollectionIdParam) = ResourceDocsAPIMethodsUtil.getParams() + + // Validate parameters + if (S.param("tags").exists(_.trim.isEmpty)) { + PlainTextResponse("Invalid tags parameter - empty values not allowed", 400) + } else if (S.param("functions").exists(_.trim.isEmpty)) { + PlainTextResponse("Invalid functions parameter - empty values not allowed", 400) + } else if (S.param("api-collection-id").exists(_.trim.isEmpty)) { + PlainTextResponse("Invalid api-collection-id parameter - empty values not allowed", 400) + } else if (S.param("content").isDefined && contentParam.isEmpty) { + PlainTextResponse("Invalid content parameter. Valid values: static, dynamic, all", 400) + } else { + try { + val requestedApiVersion = ApiVersionUtils.valueOf(requestedApiVersionString) + if (!versionIsAllowed(requestedApiVersion)) { + PlainTextResponse(s"API Version not supported: $requestedApiVersionString", 400) + } else if (locale.isDefined && APIUtil.obpLocaleValidation(locale.get) != SILENCE_IS_GOLDEN) { + PlainTextResponse(s"Invalid locale: ${locale.get}", 400) + } else { + val isVersion4OrHigher = true + val cacheKey = APIUtil.createResourceDocCacheKey( + Some("openapi31yaml"), + requestedApiVersionString, + resourceDocTags, + partialFunctions, + locale, + contentParam, + apiCollectionIdParam, + Some(isVersion4OrHigher) + ) + val cacheValueFromRedis = Caching.getStaticSwaggerDocCache(cacheKey) + + val yamlString = if (cacheValueFromRedis.isDefined) { + cacheValueFromRedis.get + } else { + // Generate OpenAPI JSON and convert to YAML + val openApiJValue = try { + val resourceDocsJsonFiltered = locale match { + case _ if (apiCollectionIdParam.isDefined) => + val operationIds = MappedApiCollectionEndpointsProvider.getApiCollectionEndpoints(apiCollectionIdParam.getOrElse("")).map(_.operationId).map(getObpFormatOperationId) + val resourceDocs = ResourceDoc.getResourceDocs(operationIds) + val resourceDocsJson = JSONFactory1_4_0.createResourceDocsJson(resourceDocs, isVersion4OrHigher, locale) + resourceDocsJson.resource_docs + case _ => + // Get all resource docs for the requested version + val allResourceDocs = ImplementationsResourceDocs.getResourceDocsList(requestedApiVersion).getOrElse(List.empty) + val filteredResourceDocs = ResourceDocsAPIMethodsUtil.filterResourceDocs(allResourceDocs, resourceDocTags, partialFunctions) + val resourceDocJson = JSONFactory1_4_0.createResourceDocsJson(filteredResourceDocs, isVersion4OrHigher, locale) + resourceDocJson.resource_docs + } + + val hostname = HostName + val openApiDoc = code.api.ResourceDocs1_4_0.OpenAPI31JSONFactory.createOpenAPI31Json(resourceDocsJsonFiltered, requestedApiVersionString, hostname) + code.api.ResourceDocs1_4_0.OpenAPI31JSONFactory.OpenAPI31JsonFormats.toJValue(openApiDoc) + } catch { + case e: Exception => + logger.error(s"Error generating OpenAPI JSON: ${e.getMessage}", e) + throw e + } + + val yamlResult = YAMLUtils.jValueToYAMLSafe(openApiJValue, s"# Error converting OpenAPI to YAML: ${openApiJValue.toString}") + Caching.setStaticSwaggerDocCache(cacheKey, yamlResult) + yamlResult + } + + val headers = List("Content-Type" -> YAMLUtils.getYAMLContentType) + val bytes = yamlString.getBytes("UTF-8") + InMemoryResponse(bytes, headers, Nil, 200) + } + } catch { + case _: Exception => + PlainTextResponse(s"Invalid API version: $requestedApiVersionString", 400) + } + } + } } } \ No newline at end of file diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala index 3d5aef287d..9d5c894b31 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala @@ -10,6 +10,7 @@ import code.api.util.ExampleValue.endpointMappingRequestBodyExample import code.api.util.FutureUtil.EndpointContext import code.api.util.NewStyle.HttpCode import code.api.util._ +import code.api.util.YAMLUtils import code.api.v1_4_0.JSONFactory1_4_0.ResourceDocsJson import code.api.v1_4_0.{APIMethods140, JSONFactory1_4_0, OBPAPI1_4_0} import code.api.v2_2_0.{APIMethods220, OBPAPI2_2_0} @@ -32,7 +33,7 @@ import com.openbankproject.commons.model.{BankId, ListResult, User} import com.openbankproject.commons.util.ApiStandards._ import com.openbankproject.commons.util.{ApiVersion, ScannedApiVersion} import net.liftweb.common.{Box, Empty, Full} -import net.liftweb.http.LiftRules +import net.liftweb.http.{InMemoryResponse, LiftRules, PlainTextResponse} import net.liftweb.json import net.liftweb.json.JsonAST.{JField, JString, JValue} import net.liftweb.json._ @@ -769,6 +770,8 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth | |This endpoint generates OpenAPI 3.1 compliant documentation with modern JSON Schema support. | + |For YAML format, use the corresponding endpoint: /resource-docs/API_VERSION/openapi.yaml + | |See the Resource Doc endpoint for more information. | |Note: Resource Docs are cached, TTL is ${GET_DYNAMIC_RESOURCE_DOCS_TTL} seconds @@ -811,6 +814,11 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth List(apiTagDocumentation, apiTagApi) ) + // Note: OpenAPI 3.1 YAML endpoint (/resource-docs/API_VERSION/openapi.yaml) + // is implemented using Lift's serve mechanism in ResourceDocs140.scala to properly + // handle YAML content type. It provides the same functionality as the JSON endpoint + // but returns OpenAPI documentation in YAML format instead of JSON. + /** * OpenAPI 3.1 endpoint with comprehensive parameter validation. * @@ -913,6 +921,25 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth } } + // Note: The OpenAPI 3.1 YAML endpoint (/resource-docs/API_VERSION/openapi.yaml) + // is implemented using Lift's serve mechanism in ResourceDocs140.scala to properly + // handle YAML content type and response format, rather than as a standard OBPEndpoint. + + + + + def convertResourceDocsToOpenAPI31YAMLAndSetCache(cacheKey: String, requestedApiVersionString: String, resourceDocsJson: List[JSONFactory1_4_0.ResourceDocJson]) : String = { + logger.debug(s"Generating OpenAPI 3.1 YAML-convertResourceDocsToOpenAPI31YAMLAndSetCache requestedApiVersion is $requestedApiVersionString") + val hostname = HostName + val openApiDoc = code.api.ResourceDocs1_4_0.OpenAPI31JSONFactory.createOpenAPI31Json(resourceDocsJson, requestedApiVersionString, hostname) + val openApiJValue = code.api.ResourceDocs1_4_0.OpenAPI31JSONFactory.OpenAPI31JsonFormats.toJValue(openApiDoc) + + val yamlString = YAMLUtils.jValueToYAMLSafe(openApiJValue, "# Error converting to YAML") + Caching.setStaticSwaggerDocCache(cacheKey, yamlString) + + yamlString + } + private def convertResourceDocsToOpenAPI31JvalueAndSetCache(cacheKey: String, requestedApiVersionString: String, resourceDocsJson: List[JSONFactory1_4_0.ResourceDocJson]) : JValue = { logger.debug(s"Generating OpenAPI 3.1-convertResourceDocsToOpenAPI31JvalueAndSetCache requestedApiVersion is $requestedApiVersionString") val hostname = HostName diff --git a/obp-api/src/main/scala/code/api/util/YAMLUtils.scala b/obp-api/src/main/scala/code/api/util/YAMLUtils.scala new file mode 100644 index 0000000000..16714ee50e --- /dev/null +++ b/obp-api/src/main/scala/code/api/util/YAMLUtils.scala @@ -0,0 +1,107 @@ +/** +Open Bank Project - API +Copyright (C) 2011-2024, TESOBE GmbH. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +Email: contact@tesobe.com +TESOBE GmbH. +Osloer Strasse 16/17 +Berlin 13359, Germany + +This product includes software developed at +TESOBE (http://www.tesobe.com/) + +*/ +package code.api.util + +import com.fasterxml.jackson.databind.{JsonNode, ObjectMapper} +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory +import net.liftweb.json.JsonAST.JValue +import net.liftweb.json._ +import net.liftweb.json.compactRender +import code.util.Helper.MdcLoggable +import scala.util.{Try, Success, Failure} + +/** + * Utility object for YAML conversion operations + * + * This utility provides methods to convert Lift's JValue objects to YAML format + * using Jackson's YAML support. + */ +object YAMLUtils extends MdcLoggable { + + private val jsonMapper = new ObjectMapper() + private val yamlMapper = new ObjectMapper(new YAMLFactory()) + + /** + * Converts a JValue to YAML string + * + * @param jValue The Lift JValue to convert + * @return Try containing the YAML string or error + */ + def jValueToYAML(jValue: JValue): Try[String] = { + Try { + // First convert JValue to JSON string + val jsonString = compactRender(jValue) + + // Parse JSON string to Jackson JsonNode + val jsonNode: JsonNode = jsonMapper.readTree(jsonString) + + // Convert JsonNode to YAML string + yamlMapper.writeValueAsString(jsonNode) + }.recoverWith { + case ex: Exception => + logger.error(s"Failed to convert JValue to YAML: ${ex.getMessage}", ex) + Failure(new RuntimeException(s"YAML conversion failed: ${ex.getMessage}", ex)) + } + } + + /** + * Converts a JValue to YAML string with error handling that returns a default value + * + * @param jValue The Lift JValue to convert + * @param defaultValue Default value to return if conversion fails + * @return YAML string or default value + */ + def jValueToYAMLSafe(jValue: JValue, defaultValue: String = ""): String = { + jValueToYAML(jValue) match { + case Success(yamlString) => yamlString + case Failure(ex) => + logger.warn(s"YAML conversion failed, returning default value: ${ex.getMessage}") + defaultValue + } + } + + /** + * Checks if the given content type indicates YAML format + * + * @param contentType The content type to check + * @return true if the content type indicates YAML + */ + def isYAMLContentType(contentType: String): Boolean = { + val normalizedContentType = contentType.toLowerCase.trim + normalizedContentType.contains("application/x-yaml") || + normalizedContentType.contains("application/yaml") || + normalizedContentType.contains("text/yaml") || + normalizedContentType.contains("text/x-yaml") + } + + /** + * Gets the appropriate YAML content type + * + * @return Standard YAML content type + */ + def getYAMLContentType: String = "application/x-yaml" +} \ No newline at end of file From 45538d0393c052d20da8f8bc24a3a09fe6a1678f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Thu, 18 Dec 2025 09:06:28 +0100 Subject: [PATCH 2/3] feature/Remove Get API Key feature from code and delegate to OBP-Portal --- ai_summary/WEBUI_PROPS_ALPHABETICAL_LIST.md | 103 ++-- .../main/scala/bootstrap/liftweb/Boot.scala | 2 +- .../main/scala/code/api/util/Glossary.scala | 20 +- .../code/snippet/ConsumerRegistration.scala | 532 ------------------ .../src/main/scala/code/snippet/Login.scala | 2 +- .../src/main/scala/code/snippet/WebUI.scala | 22 +- obp-api/src/main/scala/code/util/Helper.scala | 2 +- .../main/webapp/consumer-registration.html | 258 --------- obp-api/src/main/webapp/index-en.html | 2 +- .../webapp/templates-hidden/default-en.html | 8 +- .../templates-hidden/default-footer.html | 8 +- .../templates-hidden/default-header.html | 8 +- .../main/webapp/templates-hidden/default.html | 8 +- 13 files changed, 103 insertions(+), 872 deletions(-) delete mode 100644 obp-api/src/main/scala/code/snippet/ConsumerRegistration.scala delete mode 100644 obp-api/src/main/webapp/consumer-registration.html diff --git a/ai_summary/WEBUI_PROPS_ALPHABETICAL_LIST.md b/ai_summary/WEBUI_PROPS_ALPHABETICAL_LIST.md index 7a35174084..7f83f2f5bf 100644 --- a/ai_summary/WEBUI_PROPS_ALPHABETICAL_LIST.md +++ b/ai_summary/WEBUI_PROPS_ALPHABETICAL_LIST.md @@ -26,56 +26,57 @@ These properties can be: 13. `webui_developer_user_invitation_email_text` 14. `webui_direct_login_documentation_url` 15. `webui_dummy_user_logins` -16. `webui_faq_data_text` -17. `webui_faq_email` -18. `webui_faq_url` -19. `webui_favicon_link_url` -20. `webui_featured_sdks_external_link` -21. `webui_footer2_logo_left_url` -22. `webui_footer2_middle_text` -23. `webui_get_started_text` -24. `webui_header_logo_left_url` -25. `webui_header_logo_right_url` -26. `webui_index_page_about_section_background_image_url` -27. `webui_index_page_about_section_text` -28. `webui_legal_notice_html_text` -29. `webui_login_button_text` -30. `webui_login_page_instruction_title` -31. `webui_login_page_special_instructions` -32. `webui_main_faq_external_link` -33. `webui_main_partners` -34. `webui_main_style_sheet` -35. `webui_oauth_1_documentation_url` -36. `webui_oauth_2_documentation_url` -37. `webui_obp_cli_url` -38. `webui_override_style_sheet` -39. `webui_page_title_prefix` -40. `webui_post_consumer_registration_more_info_text` -41. `webui_post_consumer_registration_more_info_url` -42. `webui_post_consumer_registration_submit_button_value` -43. `webui_post_user_invitation_submit_button_value` -44. `webui_post_user_invitation_terms_and_conditions_checkbox_value` -45. `webui_privacy_policy` -46. `webui_privacy_policy_url` -47. `webui_sandbox_introduction` -48. `webui_sdks_url` -49. `webui_show_dummy_user_tokens` -50. `webui_signup_body_password_repeat_text` -51. `webui_signup_form_submit_button_value` -52. `webui_signup_form_title_text` -53. `webui_social_handle` -54. `webui_social_logo_url` -55. `webui_social_title` -56. `webui_social_url` -57. `webui_subscriptions_button_text` -58. `webui_subscriptions_invitation_text` -59. `webui_subscriptions_url` -60. `webui_support_email` -61. `webui_support_platform_url` -62. `webui_terms_and_conditions` -63. `webui_top_text` -64. `webui_user_invitation_notice_text` -65. `webui_vendor_support_html_url` +16. `webui_external_consumer_registration_url` +17. `webui_faq_data_text` +18. `webui_faq_email` +19. `webui_faq_url` +20. `webui_favicon_link_url` +21. `webui_featured_sdks_external_link` +22. `webui_footer2_logo_left_url` +23. `webui_footer2_middle_text` +24. `webui_get_started_text` +25. `webui_header_logo_left_url` +26. `webui_header_logo_right_url` +27. `webui_index_page_about_section_background_image_url` +28. `webui_index_page_about_section_text` +29. `webui_legal_notice_html_text` +30. `webui_login_button_text` +31. `webui_login_page_instruction_title` +32. `webui_login_page_special_instructions` +33. `webui_main_faq_external_link` +34. `webui_main_partners` +35. `webui_main_style_sheet` +36. `webui_oauth_1_documentation_url` +37. `webui_oauth_2_documentation_url` +38. `webui_obp_cli_url` +39. `webui_override_style_sheet` +40. `webui_page_title_prefix` +41. `webui_post_consumer_registration_more_info_text` +42. `webui_post_consumer_registration_more_info_url` +43. `webui_post_consumer_registration_submit_button_value` +44. `webui_post_user_invitation_submit_button_value` +45. `webui_post_user_invitation_terms_and_conditions_checkbox_value` +46. `webui_privacy_policy` +47. `webui_privacy_policy_url` +48. `webui_sandbox_introduction` +49. `webui_sdks_url` +50. `webui_show_dummy_user_tokens` +51. `webui_signup_body_password_repeat_text` +52. `webui_signup_form_submit_button_value` +53. `webui_signup_form_title_text` +54. `webui_social_handle` +55. `webui_social_logo_url` +56. `webui_social_title` +57. `webui_social_url` +58. `webui_subscriptions_button_text` +59. `webui_subscriptions_invitation_text` +60. `webui_subscriptions_url` +61. `webui_support_email` +62. `webui_support_platform_url` +63. `webui_terms_and_conditions` +64. `webui_top_text` +65. `webui_user_invitation_notice_text` +66. `webui_vendor_support_html_url` --- @@ -101,6 +102,7 @@ These properties can be: - `webui_api_explorer_url` - `webui_api_manager_url` - `webui_direct_login_documentation_url` +- `webui_external_consumer_registration_url` - `webui_faq_url` - `webui_featured_sdks_external_link` - `webui_main_faq_external_link` @@ -143,6 +145,7 @@ These properties can be: - `webui_user_invitation_notice_text` ### Consumer Registration +- `webui_external_consumer_registration_url` (defaults to `webui_api_explorer_url` + `/consumers/register`) - `webui_post_consumer_registration_more_info_text` - `webui_post_consumer_registration_more_info_url` - `webui_post_consumer_registration_submit_button_value` diff --git a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala index 00ffcecad7..ca8eceb4dc 100644 --- a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala @@ -586,7 +586,7 @@ class Boot extends MdcLoggable { Menu.i("debug-webui") / "debug" / "debug-webui", Menu.i("Consumer Admin") / "admin" / "consumers" >> Admin.loginFirst >> LocGroup("admin") submenus(Consumer.menus : _*), - Menu("Consumer Registration", Helper.i18n("consumer.registration.nav.name")) / "consumer-registration" >> AuthUser.loginFirst, + Menu("Consent Screen", Helper.i18n("consent.screen")) / "consent-screen" >> AuthUser.loginFirst, Menu("Dummy user tokens", "Get Dummy user tokens") / "dummy-user-tokens" >> AuthUser.loginFirst, diff --git a/obp-api/src/main/scala/code/api/util/Glossary.scala b/obp-api/src/main/scala/code/api/util/Glossary.scala index c9c66147ab..79d3ff77c9 100644 --- a/obp-api/src/main/scala/code/api/util/Glossary.scala +++ b/obp-api/src/main/scala/code/api/util/Glossary.scala @@ -144,11 +144,17 @@ object Glossary extends MdcLoggable { // Note: this doesn't get / use an OBP version def getApiExplorerLink(title: String, operationId: String) : String = { - val apiExplorerPrefix = APIUtil.getPropsValue("webui_api_explorer_url", "") + val apiExplorerPrefix = APIUtil.getPropsValue("webui_api_explorer_url", "http://localhost:5174") // Note: This is hardcoded for API Explorer II s"""$title""" } + // Consumer registration URL helper + def getConsumerRegistrationUrl(): String = { + val apiExplorerUrl = APIUtil.getPropsValue("webui_api_explorer_url", "http://localhost:5174") + s"$apiExplorerUrl/consumers/register" + } + glossaryItems += GlossaryItem( title = "Cheat Sheet", description = @@ -590,7 +596,7 @@ object Glossary extends MdcLoggable { | |Both standard entities (e.g. financial products and bank accounts in the OBP standard) and dynamic entities and endpoints (created by you or your organisation) can exist at the Bank level. | -|For example see [Bank/Space level Dynamic Entities](/?version=OBPv4.0.0&operation_id=OBPv4_0_0-createBankLevelDynamicEntity) and [Bank/Space level Dynamic Endpoints](http://localhost:8082/?version=OBPv4.0.0&operation_id=OBPv4_0_0-createBankLevelDynamicEndpoint) +|For example see [Bank/Space level Dynamic Entities](/?version=OBPv4.0.0&operation_id=OBPv4_0_0-createBankLevelDynamicEntity) and [Bank/Space level Dynamic Endpoints](http://localhost:5174/?version=OBPv4.0.0&operation_id=OBPv4_0_0-createBankLevelDynamicEndpoint) | |The Bank is important because many Roles can be granted at the Bank level. In this way, it's possible to create segregated or partitioned sets of endpoints and data structures in a single OBP instance. | @@ -1091,7 +1097,7 @@ object Glossary extends MdcLoggable { | |[Sign up]($getServerUrl/user_mgt/sign_up) or [login]($getServerUrl/user_mgt/login) as a developer. | - |Register your App key [HERE]($getServerUrl/consumer-registration) + |Register your App key [HERE](${getConsumerRegistrationUrl()}) | |Copy and paste the consumer key for step two below. | @@ -1182,7 +1188,7 @@ object Glossary extends MdcLoggable { | | consumer_key | The application identifier. Generated on OBP side via - | $getServerUrl/consumer-registration endpoint. + | ${getConsumerRegistrationUrl()} endpoint. | | | Each parameter MUST NOT appear more than once per request. @@ -2147,7 +2153,7 @@ object Glossary extends MdcLoggable { | |[Sign up]($getServerUrl/user_mgt/sign_up) or [login]($getServerUrl/user_mgt/login) as a developer | - |Register your App key [HERE]($getServerUrl/consumer-registration) + |Register your App key [HERE](${getConsumerRegistrationUrl()}) | |Copy and paste the CLIENT ID (AKA CONSUMER KEY), CLIENT SECRET (AKA CONSUMER SECRET) and REDIRECT_URL for the subsequent steps below. | @@ -2796,9 +2802,9 @@ object Glossary extends MdcLoggable { | |[Sign up]($getServerUrl/user_mgt/sign_up) or [login]($getServerUrl/user_mgt/login) as a developer. | -|Register your App / Consumer [HERE]($getServerUrl/consumer-registration) +|Register your App / Consumer [HERE](${getConsumerRegistrationUrl()}) | -|Be sure to enter your Client Certificate in the above form. To create the user.crt file see [HERE](https://fardog.io/blog/2017/12/30/client-side-certificate-authentication-with-nginx/) +|Be sure to enter your Client Certificate in the registration form. To create the user.crt file see [HERE](https://fardog.io/blog/2017/12/30/client-side-certificate-authentication-with-nginx/) | | |## Authenticate diff --git a/obp-api/src/main/scala/code/snippet/ConsumerRegistration.scala b/obp-api/src/main/scala/code/snippet/ConsumerRegistration.scala deleted file mode 100644 index daddd1a293..0000000000 --- a/obp-api/src/main/scala/code/snippet/ConsumerRegistration.scala +++ /dev/null @@ -1,532 +0,0 @@ -/** -Open Bank Project - API -Copyright (C) 2011-2019, TESOBE GmbH. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . - -Email: contact@tesobe.com -TESOBE GmbH. -Osloer Strasse 16/17 -Berlin 13359, Germany - -This product includes software developed at -TESOBE (http://www.tesobe.com/) - - */ -package code.snippet - -import java.util -import code.api.{Constant, DirectLogin} -import code.api.util.{APIUtil, ErrorMessages, KeycloakAdmin, X509, CommonsEmailWrapper} -import code.consumer.Consumers -import code.model.dataAccess.AuthUser -import code.model.{Consumer, _} -import code.util.Helper.{MdcLoggable, ObpS} -import code.util.HydraUtil -import code.webuiprops.MappedWebUiPropsProvider.getWebUiPropsValue -import net.liftweb.common.{Box, Failure, Full} -import net.liftweb.http.{RequestVar, S, SHtml} -import net.liftweb.util.Helpers._ -import net.liftweb.util.{CssSel, FieldError, Helpers} -import org.apache.commons.lang3.StringUtils -import org.codehaus.jackson.map.ObjectMapper - -import scala.collection.immutable.{List, ListMap} -import scala.collection.JavaConverters._ -import scala.xml.{Text, Unparsed} - -class ConsumerRegistration extends MdcLoggable { - - private object nameVar extends RequestVar("") - private object redirectionURLVar extends RequestVar("") - private object requestUriVar extends RequestVar("") - private object authenticationURLVar extends RequestVar("") - private object appTypeVar extends RequestVar[AppType](AppType.Confidential) - private object descriptionVar extends RequestVar("") - private object devEmailVar extends RequestVar("") - private object companyVar extends RequestVar("") - private object appType extends RequestVar("Public") - private object clientCertificateVar extends RequestVar("") - private object signingAlgVar extends RequestVar("") - private object oidcCheckboxVar extends RequestVar(false) - private object jwksUriVar extends RequestVar("") - private object jwksVar extends RequestVar("") - private object submitButtonDefenseFlag extends RequestVar("") - - - - - // Can be used to show link to an online form to collect more information about the App / Startup - val registrationMoreInfoUrl = getWebUiPropsValue("webui_post_consumer_registration_more_info_url", "") - - val registrationConsumerButtonValue = getWebUiPropsValue("webui_post_consumer_registration_submit_button_value", "Register consumer") - - val registrationMoreInfoText : String = registrationMoreInfoUrl match { - case "" => "" - case _ => getWebUiPropsValue("webui_post_consumer_registration_more_info_text", "Please tell us more your Application and / or Startup using this link.") - } - - - def registerForm = { - - val appTypes = List((AppType.Confidential.toString, AppType.Confidential.toString), (AppType.Public.toString, AppType.Public.toString)) - val signingAlgs = List( - "ES256", "ES384", "ES512", - //Hydra support alg: RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384 and ES512 - "RS256", "RS384", "RS512", "PS256", "PS384", "PS512" - ).map(it => it -> it) - - def submitButtonDefense: Unit = { - submitButtonDefenseFlag("true") - } - - def registerWithoutWarnings = - register & - "#register-consumer-errors" #> "" - - def displayAppType: Boolean = APIUtil.getPropsAsBoolValue("consumer_registration.display_app_type", true) - - def register = { - "form" #> { - "#app-type-div [style] " #> {if(displayAppType) "display: block;" else "display: none"} & - "#appType" #> SHtml.select(appTypes, Box!! appType.is, appType(_)) & - "#appName" #> SHtml.text(nameVar.is, nameVar(_)) & - "#redirect_url_label *" #> { - if (HydraUtil.integrateWithHydra) "Redirect URL" else "Redirect URL (Optional)" - } & - "#appRedirectUrl" #> SHtml.text(redirectionURLVar, redirectionURLVar(_)) & - "#appDev" #> SHtml.text(devEmailVar, devEmailVar(_)) & - "#company" #> SHtml.text(companyVar, companyVar(_)) & - "#appDesc" #> SHtml.textarea(descriptionVar, descriptionVar (_)) & - "#appUserAuthenticationUrl" #> SHtml.text(authenticationURLVar.is, authenticationURLVar(_)) & { - if(HydraUtil.integrateWithHydra) { - "#app-client_certificate" #> SHtml.textarea(clientCertificateVar, clientCertificateVar (_))& - "#app-request_uri" #> SHtml.text(requestUriVar, requestUriVar(_)) & - "#oidc_checkbox" #> SHtml.checkbox(oidcCheckboxVar, oidcCheckboxVar(_)) & - "#app-signing_alg" #> SHtml.select(signingAlgs, Box!! signingAlgVar.is, signingAlgVar(_)) & - "#app-jwks_uri" #> SHtml.text(jwksUriVar, jwksUriVar(_)) & - "#app-jwks" #> SHtml.textarea(jwksVar, jwksVar(_)) - } else { - ".oauth2_fields" #> "" - } - } & - "type=submit" #> SHtml.submit(s"$registrationConsumerButtonValue", () => submitButtonDefense) - } & - "#register-consumer-success" #> "" - } - - def showResults(consumer : Consumer) = { - val urlOAuthEndpoint = Constant.HostName + "/oauth/initiate" - val urlDirectLoginEndpoint = Constant.HostName + "/my/logins/direct" - val jwksUri = jwksUriVar.is - val jwks = jwksVar.is - val jwsAlg = signingAlgVar.is - var jwkPrivateKey: String = s"Please change this value to ${if(StringUtils.isNotBlank(jwksUri)) "jwks_uri" else "jwks"} corresponding private key" - // In case we use Hydra ORY as Identity Provider we create corresponding client at Hydra side a well - if(HydraUtil.integrateWithHydra) { - HydraUtil.createHydraClient(consumer, oAuth2Client => { - val signingAlg = signingAlgVar.is - - if(oidcCheckboxVar.is == false) { - // TODO Set token_endpoint_auth_method in accordance to the Consumer.AppType value - // Consumer.AppType = Confidential => client_secret_post - // Consumer.AppType = Public => private_key_jwt - // Consumer.AppType = Unknown => private_key_jwt - oAuth2Client.setTokenEndpointAuthMethod(HydraUtil.hydraTokenEndpointAuthMethod) - } else { - oAuth2Client.setTokenEndpointAuthMethod(HydraUtil.clientSecretPost) - } - - - oAuth2Client.setTokenEndpointAuthSigningAlg(signingAlg) - oAuth2Client.setRequestObjectSigningAlg(signingAlg) - - def toJson(jwksJson: String) = - new ObjectMapper().readValue(jwksJson, classOf[util.Map[String, _]]) - - val requestUri = requestUriVar.is - if(StringUtils.isAllBlank(jwksUri, jwks)) { - val(privateKey, publicKey) = HydraUtil.createJwk(signingAlg) - jwkPrivateKey = privateKey - val jwksJson = s"""{"keys": [$publicKey]}""" - val jwksMap = toJson(jwksJson) - oAuth2Client.setJwks(jwksMap) - } else if(StringUtils.isNotBlank(jwks)){ - val jwksMap = toJson(jwks) - oAuth2Client.setJwks(jwksMap) - } else if(StringUtils.isNotBlank(jwksUri)){ - oAuth2Client.setJwksUri(jwksUri) - } - - if(StringUtils.isNotBlank(requestUri)) { - oAuth2Client.setRequestUris(List(requestUri).asJava) - } - oAuth2Client - }) - } - - // In case we use Keycloak as Identity Provider we create corresponding client at Keycloak side a well - if(KeycloakAdmin.integrateWithKeycloak) KeycloakAdmin.createKeycloakConsumer(consumer) - - val registerConsumerSuccessMessageWebpage = getWebUiPropsValue( - "webui_register_consumer_success_message_webpage", - "Thanks for registering your consumer with the Open Bank Project API! Here is your developer information. Please save it in a secure location.") - //thanks for registering, here's your key, etc. - "#register-consumer-success-message *" #> registerConsumerSuccessMessageWebpage & - "#app-consumer_id *" #> consumer.consumerId.get & - "#app-name *" #> consumer.name.get & - "#app-redirect-url *" #> consumer.redirectURL & - "#app-user-authentication-url *" #> consumer.userAuthenticationURL & - "#app-type *" #> consumer.appType.get & - "#app-description *" #> consumer.description.get & - "#client_certificate *" #> { - if (StringUtils.isBlank(consumer.clientCertificate.get)) Text("None") - else Unparsed(consumer.clientCertificate.get) - } & - "#app-developer *" #> consumer.developerEmail.get & - "#auth-key *" #> consumer.key.get & - "#secret-key *" #> consumer.secret.get & - "#oauth-endpoint a *" #> urlOAuthEndpoint & - "#oauth-endpoint a [href]" #> urlOAuthEndpoint & - "#directlogin-endpoint a *" #> urlDirectLoginEndpoint & - "#directlogin-endpoint a [href]" #> urlDirectLoginEndpoint & - "#post-consumer-registration-more-info-link a *" #> registrationMoreInfoText & - "#post-consumer-registration-more-info-link a [href]" #> registrationMoreInfoUrl & { - if(HydraUtil.integrateWithHydra) { - "#hydra-client-info-title *" #>"OAuth2: " & - "#admin_url *" #> HydraUtil.hydraAdminUrl & - "#client_id *" #> {consumer.key.get} & - "#redirect_uri *" #> consumer.redirectURL.get & - { - val requestUri = requestUriVar.is - if(StringUtils.isBlank(requestUri)) "#oauth2_request_uri *" #> "" - else "#request_uri_value" #> requestUri - } & - "#client_scope" #> { - val lastIndex = HydraUtil.hydraConsents.length - 1 - HydraUtil.hydraConsents.zipWithIndex.map { kv => - ".client-scope-value *" #> { - val (scope, index) = kv - if(index == lastIndex) { - scope - } else { - s"$scope,\\" - } - } - } - } & - "#client_jws_alg" #> Unparsed(jwsAlg) & - "#jwk_private_key" #> Unparsed(jwkPrivateKey) - } else { - "#hydra-client-info-title *" #> "" & - "#hydra-client-info *" #> "" - } - } & - "#register-consumer-input" #> "" & { - val hasDummyUsers = getWebUiPropsValue("webui_dummy_user_logins", "").nonEmpty - val isShowDummyUserTokens = getWebUiPropsValue("webui_show_dummy_user_tokens", "false").toBoolean - if(hasDummyUsers && isShowDummyUserTokens) { - "#create-directlogin a [href]" #> s"dummy-user-tokens?consumer_key=${consumer.key.get}" - } else { - "#dummy-user-tokens" #> "" - } - } - } - - def showRegistrationResults(result : Consumer) = { - - notifyRegistrationOccurred(result) - sendEmailToDeveloper(result) - - showResults(result) - } - - def showErrors(errors : List[FieldError]) = { - val errorsString = errors.map(_.msg.toString) - errorsString.map(errorMessage => S.error("register-consumer-errors", errorMessage)) - register & - "#register-consumer-errors *" #> { - ".error *" #> - errorsString.map({ e=> - ".errorContent *" #> e - }) - } - } - - def showUnknownErrors(errors : List[String]) = { - errors.map(errorMessage => S.error("register-consumer-errors", errorMessage)) - register & - "#register-consumer-errors *" #> { - ".error *" #> - errors.map({ e=> - ".errorContent *" #> e - }) - } - } - def showValidationErrors(errors : List[String]): CssSel = { - errors.filter(errorMessage => (errorMessage.contains("name") || errorMessage.contains("Name")) ).map(errorMessage => S.error("consumer-registration-app-name-error", errorMessage)) - errors.filter(errorMessage => (errorMessage.contains("description") || errorMessage.contains("Description"))).map(errorMessage => S.error("consumer-registration-app-description-error", errorMessage)) - errors.filter(errorMessage => (errorMessage.contains("email")|| errorMessage.contains("Email"))).map(errorMessage => S.error("consumer-registration-app-developer-error", errorMessage)) - errors.filter(errorMessage => (errorMessage.contains("redirect")|| errorMessage.contains("Redirect"))).map(errorMessage => S.error("consumer-registration-app-redirect-url-error", errorMessage)) - errors.filter(errorMessage => errorMessage.contains("request_uri")).map(errorMessage => S.error("consumer-registration-app-request_uri-error", errorMessage)) - errors.filter(errorMessage => StringUtils.containsAny(errorMessage, "signing_alg", "jwks_uri", "jwks")) - .map(errorMessage => S.error("consumer-registration-app-signing_jwks-error", errorMessage)) - errors.filter(errorMessage => errorMessage.contains("certificate")).map(errorMessage => S.error("consumer-registration-app-client_certificate-error", errorMessage)) - //Here show not field related errors to the general part. - val unknownErrors: Seq[String] = errors - .filterNot(errorMessage => (errorMessage.contains("name") || errorMessage.contains("Name"))) - .filterNot(errorMessage => (errorMessage.contains("description") || errorMessage.contains("Description"))) - .filterNot(errorMessage => (errorMessage.contains("email") || errorMessage.contains("Email"))) - .filterNot(errorMessage => (errorMessage.contains("redirect") || errorMessage.contains("Redirect"))) - unknownErrors.map(errorMessage => S.error("register-consumer-errors", errorMessage)) - register & - "#register-consumer-errors *" #> { - ".error *" #> - unknownErrors.map({ e=> - ".errorContent *" #> e - }) - } - } - - //TODO this should be used somewhere else, it is check the empty of description for the hack attack from GUI. - def showErrorsForDescription (descriptionError : String) = { - S.error("register-consumer-errors", descriptionError) - register & - "#register-consumer-errors *" #> { - ".error *" #> - List(descriptionError).map({ e=> - ".errorContent *" #> e - }) - } - } - - def analyseResult = { - - def withNameOpt(s: String): Option[AppType] = Some(AppType.valueOf(s)) - - val clientCertificate = clientCertificateVar.is - val requestUri = requestUriVar.is - val signingAlg = signingAlgVar.is - val jwksUri = jwksUriVar.is - val jwks = jwksVar.is - - val appTypeSelected = withNameOpt(appType.is) - logger.debug("appTypeSelected: " + appTypeSelected) - nameVar.set(nameVar.is) - appTypeVar.set(appTypeSelected.get) - descriptionVar.set(descriptionVar.is) - devEmailVar.set(devEmailVar.is) - companyVar.set(companyVar.is) - redirectionURLVar.set(redirectionURLVar.is) - - requestUriVar.set(requestUri) - clientCertificateVar.set(clientCertificate) - signingAlgVar.set(signingAlg) - jwksUriVar.set(jwksUri) - jwksVar.set(jwks) - - val oauth2ParamError: CssSel = if(HydraUtil.integrateWithHydra) { - if(StringUtils.isBlank(redirectionURLVar.is) || Consumer.redirectURLRegex.findFirstIn(redirectionURLVar.is).isEmpty) { - showErrorsForDescription("The 'Redirect URL' should be a valid url !") - } else if(StringUtils.isNotBlank(requestUri) && !requestUri.matches("""^https?://(www.)?\S+?(:\d{2,6})?\S*$""")) { - showErrorsForDescription("The 'request_uri' should be a valid url !") - } else if(StringUtils.isNotBlank(jwksUri) && !jwksUri.matches("""^https?://(www.)?\S+?(:\d{2,6})?\S*$""")) { - showErrorsForDescription("The 'jwks_uri' should be a valid url !") - } else if(StringUtils.isBlank(signingAlg)) { - showErrorsForDescription("The 'signing_alg' should not be empty!") - } else if(StringUtils.isNoneBlank(jwksUri, jwks)) { - showErrorsForDescription("The 'jwks_uri' and 'jwks' should not have value at the same time!") - } else if (StringUtils.isNotBlank(clientCertificate) && X509.validate(clientCertificate) != Full(true)) { - showErrorsForDescription("The 'client certificate' should be a valid certificate, pleas copy whole crt file content !") - } else null - } else null - - if(oauth2ParamError != null) { - oauth2ParamError - } else if(submitButtonDefenseFlag.isEmpty) { - showErrorsForDescription("The 'Register' button random name has been modified !") - } else{ - val appType = - if(displayAppType) appTypeSelected - else Some(AppType.Unknown) // If Application Type is hidden from Consumer registration it defaults to Unknown - val consumer = Consumers.consumers.vend.createConsumer( - Some(Helpers.randomString(40).toLowerCase), - Some(Helpers.randomString(40).toLowerCase), - Some(true), - Some(nameVar.is), - appType, - Some(descriptionVar.is), - Some(devEmailVar.is), - Some(redirectionURLVar.is), - Some(AuthUser.getCurrentResourceUserUserId), - Some(clientCertificate), - company = Some(companyVar.is), - None - ) - logger.debug("consumer: " + consumer) - consumer match { - case Full(x) => - showRegistrationResults(x) - case Failure(msg, _, _) => showValidationErrors(msg.split(";").toList) - case _ => showUnknownErrors(List(ErrorMessages.UnknownError)) - } - } - } - - if(S.post_?) analyseResult - else registerWithoutWarnings - - } - - def sendEmailToDeveloper(registered : Consumer) = { - import net.liftweb.util.Mailer - import net.liftweb.util.Mailer._ - - val mailSent = for { - send : String <- APIUtil.getPropsValue("mail.api.consumer.registered.notification.send") if send.equalsIgnoreCase("true") - from <- APIUtil.getPropsValue("mail.api.consumer.registered.sender.address") ?~ "Could not send mail: Missing props param for 'from'" - } yield { - - // Only send consumer key / secret by email if we explicitly want that. - val sendSensitive : Boolean = APIUtil.getPropsAsBoolValue("mail.api.consumer.registered.notification.send.sensistive", false) - val consumerKeyOrMessage : String = if (sendSensitive) registered.key.get else "Configured so sensitive data is not sent by email (Consumer Key)." - val consumerSecretOrMessage : String = if (sendSensitive) registered.secret.get else "Configured so sensitive data is not sent by email (Consumer Secret)." - - val thisApiInstance = Constant.HostName - val apiExplorerUrl = getWebUiPropsValue("webui_api_explorer_url", "unknown host") - val directLoginDocumentationUrl = getWebUiPropsValue("webui_direct_login_documentation_url", apiExplorerUrl + "/glossary#Direct-Login") - val oauthDocumentationUrl = getWebUiPropsValue("webui_oauth_1_documentation_url", apiExplorerUrl + "/glossary#OAuth-1.0a") - val oauthEndpointUrl = thisApiInstance + "/oauth/initiate" - - val directLoginEndpointUrl = thisApiInstance + "/my/logins/direct" - val registrationMessage = s"Thank you for registering a Consumer on $thisApiInstance. \n" + - s"Email: ${registered.developerEmail.get} \n" + - s"App name: ${registered.name.get} \n" + - s"App type: ${registered.appType.get} \n" + - s"App description: ${registered.description.get} \n" + - s"App Redirect Url : ${registered.redirectURL} \n" + - s"Consumer Key: ${consumerKeyOrMessage} \n" + - s"Consumer Secret : ${consumerSecretOrMessage} \n" + - s"OAuth Endpoint: ${oauthEndpointUrl} \n" + - s"OAuth Documentation: ${directLoginDocumentationUrl} \n" + - s"Direct Login Endpoint: ${directLoginEndpointUrl} \n" + - s"Direct Login Documentation: ${oauthDocumentationUrl} \n" + - s"$registrationMoreInfoText: $registrationMoreInfoUrl" - - val webuiRegisterConsumerSuccessMssageEmail : String = getWebUiPropsValue( - "webui_register_consumer_success_message_email", - "Thank you for registering to use the Open Bank Project API.") - - val emailContent = CommonsEmailWrapper.EmailContent( - from = from, - to = List(registered.developerEmail.get), - subject = webuiRegisterConsumerSuccessMssageEmail, - textContent = Some(registrationMessage) - ) - - //this is an async call - CommonsEmailWrapper.sendTextEmail(emailContent) - } - - if(mailSent.isEmpty) - this.logger.warn(s"Sending email with API consumer registration data is omitted: $mailSent") - - } - - // This is to let the system administrators / API managers know that someone has registered a consumer key. - def notifyRegistrationOccurred(registered : Consumer) = { - - val mailSent = for { - // e.g mail.api.consumer.registered.sender.address=no-reply@example.com - from <- APIUtil.getPropsValue("mail.api.consumer.registered.sender.address") ?~ "Could not send mail: Missing props param for 'from'" - // no spaces, comma separated e.g. mail.api.consumer.registered.notification.addresses=notify@example.com,notify2@example.com,notify3@example.com - toAddressesString <- APIUtil.getPropsValue("mail.api.consumer.registered.notification.addresses") ?~ "Could not send mail: Missing props param for 'to'" - } yield { - - val thisApiInstance = Constant.HostName - val registrationMessage = s"New user signed up for API keys on $thisApiInstance. \n" + - s"Email: ${registered.developerEmail.get} \n" + - s"App name: ${registered.name.get} \n" + - s"App type: ${registered.appType.get} \n" + - s"App description: ${registered.description.get} \n" + - s"App Redirect Url : ${registered.redirectURL}" - - //technically doesn't work for all valid email addresses so this will mess up if someone tries to send emails to "foo,bar"@example.com - val to = toAddressesString.split(",").toList - - val emailContent = CommonsEmailWrapper.EmailContent( - from = from, - to = to, - subject = s"New API user registered on $thisApiInstance", - textContent = Some(registrationMessage) - ) - - //this is an async call - CommonsEmailWrapper.sendTextEmail(emailContent) - } - - if(mailSent.isEmpty) - this.logger.warn(s"API consumer registration failed: $mailSent") - - } - - def showDummyCustomerTokens(): CssSel = { - val consumerKeyBox = ObpS.param("consumer_key") - // The following will check the login user and the user from the consumerkey. we do not want to share consumerkey to others. - val loginUserId = AuthUser.getCurrentUser.map(_.userId).openOr("") - val userCreatedByUserId = consumerKeyBox.map(Consumers.consumers.vend.getConsumerByConsumerKey(_)).flatten.map(_.createdByUserId.get).openOr("") - if(!loginUserId.equals(userCreatedByUserId)) - return "#dummy-user-tokens ^" #> "The consumer key in the URL is not created by the current login user, please create consumer for this user first!" - - val dummyUsersInfo = getWebUiPropsValue("webui_dummy_user_logins", "") - val isShowDummyUserTokens = getWebUiPropsValue("webui_show_dummy_user_tokens", "false").toBoolean - // (username, password) -> authHeader - val userNameToAuthInfo: Map[(String, String), String] = (isShowDummyUserTokens, consumerKeyBox, dummyUsersInfo) match { - case(true, Full(consumerKey), dummyCustomers) if dummyCustomers.nonEmpty => { - val regex = """(?s)\{.*?"user_name"\s*:\s*"(.+?)".+?"password"\s*:\s*"(.+?)".+?\}""".r - val matcher = regex.pattern.matcher(dummyCustomers) - var tokens = ListMap[(String, String), String]() - while(matcher.find()) { - val userName = matcher.group(1) - val password = matcher.group(2) - val (code, token, userId) = DirectLogin.createToken(Map(("username", userName), ("password", password), ("consumer_key", consumerKey))) - val authHeader = code match { - case 200 => (userName, password) -> s"""Authorization: DirectLogin token="$token"""" - case _ => (userName, password) -> "username or password is invalid, generate token fail" - } - tokens += authHeader - } - tokens - } - case _ => Map.empty[(String, String), String] - } - - val elements = userNameToAuthInfo.map{ pair => - val ((userName, password), authHeader) = pair -
-
- username:
- {userName}
- password:
- {password} -
-
- {authHeader} -
-
- } - - "#dummy-user-tokens ^" #> elements - } -} diff --git a/obp-api/src/main/scala/code/snippet/Login.scala b/obp-api/src/main/scala/code/snippet/Login.scala index 0ce1da97ce..a7c6a36c34 100644 --- a/obp-api/src/main/scala/code/snippet/Login.scala +++ b/obp-api/src/main/scala/code/snippet/Login.scala @@ -48,7 +48,7 @@ class Login { } else { ".logout [href]" #> { if(APIUtil.getPropsAsBoolValue("sso.enabled", false)) { - val apiExplorerUrl = getWebUiPropsValue("webui_api_explorer_url", "http://localhost:8082") + val apiExplorerUrl = getWebUiPropsValue("webui_api_explorer_url", "http://localhost:5174") apiExplorerUrl + "/obp-api-logout" } else { AuthUser.logoutPath.foldLeft("")(_ + "/" + _) diff --git a/obp-api/src/main/scala/code/snippet/WebUI.scala b/obp-api/src/main/scala/code/snippet/WebUI.scala index 016d1d1f3f..63214fa925 100644 --- a/obp-api/src/main/scala/code/snippet/WebUI.scala +++ b/obp-api/src/main/scala/code/snippet/WebUI.scala @@ -235,7 +235,7 @@ class WebUI extends MdcLoggable{ val tags = S.attr("tags") openOr "" val locale = S.locale.toString // Note the Props value might contain a query parameter e.g. ?psd2=true - val baseUrl = getWebUiPropsValue("webui_api_explorer_url", "") + val baseUrl = getWebUiPropsValue("webui_api_explorer_url", "http://localhost:5174") // hack (we should use url operators instead) so we can add further query parameters if one is already included in the the baseUrl val baseUrlWithQuery = baseUrl.contains("?") match { case true => baseUrl + s"&tags=$tags${brandString}&locale=${locale}" // ? found so add & instead @@ -309,7 +309,19 @@ class WebUI extends MdcLoggable{ ".commit-id-link a [href]" #> s"https://github.com/OpenBankProject/OBP-API/commit/$commitId" } - + // External Consumer Registration Link + // This replaces the internal Lift-based consumer registration functionality + // with a link to an external consumer registration service. + // Uses webui_api_explorer_url + /consumers/register as default. + // Configure webui_external_consumer_registration_url to override with a custom URL. + def externalConsumerRegistrationLink: CssSel = { + val apiExplorerUrl = getWebUiPropsValue("webui_api_explorer_url", "http://localhost:5174") + val defaultConsumerRegisterUrl = s"$apiExplorerUrl/consumers/register" + val externalUrl = getWebUiPropsValue("webui_external_consumer_registration_url", defaultConsumerRegisterUrl) + ".get-api-key-link a [href]" #> scala.xml.Unparsed(externalUrl) & + ".get-api-key-link a [target]" #> "_blank" & + ".get-api-key-link a [rel]" #> "noopener" + } // Social Finance (Sofi) def sofiLink: CssSel = { @@ -456,7 +468,7 @@ class WebUI extends MdcLoggable{ } // API Explorer URL from Props - val apiExplorerUrl = scala.xml.Unparsed(getWebUiPropsValue("webui_api_explorer_url", "")) + val apiExplorerUrl = scala.xml.Unparsed(getWebUiPropsValue("webui_api_explorer_url", "http://localhost:5174")) // DirectLogin documentation url def directLoginDocumentationUrl: CssSel = { @@ -491,13 +503,13 @@ class WebUI extends MdcLoggable{ def directLoginDocLink: CssSel = { - val baseUrl = getWebUiPropsValue("webui_api_explorer_url", "") + val baseUrl = getWebUiPropsValue("webui_api_explorer_url", "http://localhost:5174") val supportplatformlink = scala.xml.Unparsed(getWebUiPropsValue("webui_direct_login_documentation_url", s"${baseUrl}/glossary#Direct-Login")) "#direct-login-doc-link a [href]" #> supportplatformlink } def oauth1aLoginDocLink: CssSel = { - val baseUrl = getWebUiPropsValue("webui_api_explorer_url", "") + val baseUrl = getWebUiPropsValue("webui_api_explorer_url", "http://localhost:5174") val supportplatformlink = scala.xml.Unparsed(getWebUiPropsValue("webui_oauth_1_documentation_url", s"${baseUrl}/glossary#OAuth-1.0a")) "#oauth1a-doc-link a [href]" #> supportplatformlink } diff --git a/obp-api/src/main/scala/code/util/Helper.scala b/obp-api/src/main/scala/code/util/Helper.scala index 6ffaaf168c..e2dd615629 100644 --- a/obp-api/src/main/scala/code/util/Helper.scala +++ b/obp-api/src/main/scala/code/util/Helper.scala @@ -216,7 +216,7 @@ object Helper extends Loggable { def isValidInternalRedirectUrl(url: String) : Boolean = { //set the default value is "/" and "/oauth/authorize" val internalRedirectUrlsWhiteList = List( - "/","/oauth/authorize","/consumer-registration", + "/","/oauth/authorize", "/dummy-user-tokens","/create-sandbox-account", "/add-user-auth-context-update-request","/otp", "/terms-and-conditions", "/privacy-policy", diff --git a/obp-api/src/main/webapp/consumer-registration.html b/obp-api/src/main/webapp/consumer-registration.html deleted file mode 100644 index a117651244..0000000000 --- a/obp-api/src/main/webapp/consumer-registration.html +++ /dev/null @@ -1,258 +0,0 @@ - -
-
- -
-
-

Register your consumer

-

Please complete the information about your application below, so we can create your OAuth consumer key and secret.

-

All fields are required unless marked as 'optional'

-
- - - - - - - -
-
-
-
- - -
-
- - -
- -
-
-
- - i - -
- -
-
-
- - -
- -
-
-
- - -
- -
-
-
- - -
- -
-
-
-
- -
-
-

OAuth2 related:

- -
-
- -
-
- The signing algorithm name of request object and client_assertion. - Reference 6.1. Passing a Request Object by Value - and 9. Client Authentication -
-
- -
-
- - -
- -
-
-
- - -
-
- -
-
- Content of jwks_uri. jwks_uri and jwks should not both have value at the same time. - Reference 10.1.1. Rotation of Asymmetric Signing Keys -
-
- -
- -
-
-
- - -
- -
-
-
- - -
-
-
- -
- -
-
-
-
-

Register your consumer

-
-

Thanks for registering your consumer with the Open Bank API! Here is your developer information. Please save it in a secure location.

-
-
-

Please save it in a secure location.

-
-
-
-
-
Consumer ID:
-
123
-
-
-
Application Type:
-
web
-
-
-
Application Name:
-
ABC
-
-
-
User redirect URL:
-
ABC
-
-
-
Developer Email:
-
abc@example.com
-
-
-
App Description:
-
ABCDEF
-
-
-
Client certificate:
-
- ABCDEF -
-
-
-
Consumer Key:
-
23432432432432
-
-
-
Consumer Secret:
-
3334543543543
-
-
-
OAuth 1.0a Endpoint:
- -
-
-
OAuth 1.0a Documentation:
- -
-
-
Dummy Users' Direct Login Tokens:
- -
-
-
Direct Login Endpoint:
- -
-
-
Direct Login Documentation:
- -
-
-
-
-
-
-
OAuth2:
-
- - oauth2.client_id=auth-code-client
- oauth2.redirect_uri=http://127.0.0.1:8081/main.html
- - oauth2.request_uri=http://127.0.0.1:8081/request_object.json
-
- oauth2.client_scope=ReadAccountsBasic

- oauth2.jws_alg=
- oauth2.jwk_private_key=content of jwk key
-
-
-
-
-
-
diff --git a/obp-api/src/main/webapp/index-en.html b/obp-api/src/main/webapp/index-en.html index e3d6a2be4d..a0dd0210ca 100644 --- a/obp-api/src/main/webapp/index-en.html +++ b/obp-api/src/main/webapp/index-en.html @@ -37,7 +37,7 @@

We View API Explorer Introduction - + Get API key diff --git a/obp-api/src/main/webapp/templates-hidden/default-en.html b/obp-api/src/main/webapp/templates-hidden/default-en.html index 66021d77de..2b2e4f8313 100644 --- a/obp-api/src/main/webapp/templates-hidden/default-en.html +++ b/obp-api/src/main/webapp/templates-hidden/default-en.html @@ -119,8 +119,8 @@ API Explorer -
  • -
  • diff --git a/obp-api/src/main/webapp/templates-hidden/default-footer.html b/obp-api/src/main/webapp/templates-hidden/default-footer.html index 91bee85c26..74a60838bb 100644 --- a/obp-api/src/main/webapp/templates-hidden/default-footer.html +++ b/obp-api/src/main/webapp/templates-hidden/default-footer.html @@ -124,8 +124,8 @@ API Explorer
  • -
  • -
  • diff --git a/obp-api/src/main/webapp/templates-hidden/default-header.html b/obp-api/src/main/webapp/templates-hidden/default-header.html index 96384d792d..fba6bbb16d 100644 --- a/obp-api/src/main/webapp/templates-hidden/default-header.html +++ b/obp-api/src/main/webapp/templates-hidden/default-header.html @@ -119,8 +119,8 @@ API Explorer
  • -
  • -
  • diff --git a/obp-api/src/main/webapp/templates-hidden/default.html b/obp-api/src/main/webapp/templates-hidden/default.html index 16d92c555f..4eb5915caa 100644 --- a/obp-api/src/main/webapp/templates-hidden/default.html +++ b/obp-api/src/main/webapp/templates-hidden/default.html @@ -119,8 +119,8 @@ API Explorer
  • -
  • -
  • From 4dda540f38fb5ac3cfcaf30db84d1d9856c3c101 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Thu, 18 Dec 2025 12:39:55 +0100 Subject: [PATCH 3/3] feature/Improve Pekko find available port --- .../code/actorsystem/ObpActorConfig.scala | 41 ++++++++++++++----- .../akka/actor/AkkaConnectorActorConfig.scala | 40 ++++++++++++------ .../test/scala/code/setup/ServerSetup.scala | 5 +++ 3 files changed, 63 insertions(+), 23 deletions(-) diff --git a/obp-api/src/main/scala/code/actorsystem/ObpActorConfig.scala b/obp-api/src/main/scala/code/actorsystem/ObpActorConfig.scala index 848e2efd65..996d3b2366 100644 --- a/obp-api/src/main/scala/code/actorsystem/ObpActorConfig.scala +++ b/obp-api/src/main/scala/code/actorsystem/ObpActorConfig.scala @@ -7,7 +7,14 @@ import code.util.Helper object ObpActorConfig { val localHostname = "127.0.0.1" - def localPort = Helper.findAvailablePort() + def localPort = { + val systemPort = APIUtil.getPropsAsIntValue("pekko.remote.artery.canonical.port", 0) + if (systemPort == 0) { + Helper.findAvailablePort() + } else { + systemPort + } + } val akka_loglevel = APIUtil.getPropsValue("remotedata.loglevel").openOr("INFO") @@ -64,12 +71,16 @@ object ObpActorConfig { } } remote { - enabled-transports = ["org.apache.pekko.remote.netty.tcp"] - netty { - tcp { - send-buffer-size = 50000000 - receive-buffer-size = 50000000 - maximum-frame-size = 52428800 + artery { + transport = tcp + canonical.hostname = """ + localHostname + """ + canonical.port = 0 + bind.hostname = """ + localHostname + """ + bind.port = 0 + advanced { + maximum-frame-size = 52428800 + buffer-pool-size = 128 + maximum-large-frame-size = 52428800 } } } @@ -80,8 +91,12 @@ object ObpActorConfig { s""" ${commonConf} pekko { - remote.netty.tcp.hostname = ${localHostname} - remote.netty.tcp.port = 0 + remote.artery { + canonical.hostname = ${localHostname} + canonical.port = 0 + bind.hostname = ${localHostname} + bind.port = 0 + } } """ @@ -89,8 +104,12 @@ object ObpActorConfig { s""" ${commonConf} pekko { - remote.netty.tcp.hostname = ${localHostname} - remote.netty.tcp.port = ${localPort} + remote.artery { + canonical.hostname = ${localHostname} + canonical.port = ${localPort} + bind.hostname = ${localHostname} + bind.port = ${localPort} + } } """ } diff --git a/obp-api/src/main/scala/code/bankconnectors/akka/actor/AkkaConnectorActorConfig.scala b/obp-api/src/main/scala/code/bankconnectors/akka/actor/AkkaConnectorActorConfig.scala index ca811607b1..1925ce9d4b 100644 --- a/obp-api/src/main/scala/code/bankconnectors/akka/actor/AkkaConnectorActorConfig.scala +++ b/obp-api/src/main/scala/code/bankconnectors/akka/actor/AkkaConnectorActorConfig.scala @@ -67,12 +67,16 @@ object AkkaConnectorActorConfig { } } remote { - enabled-transports = ["org.apache.pekko.remote.netty.tcp"] - netty { - tcp { - send-buffer-size = 50000000 - receive-buffer-size = 50000000 - maximum-frame-size = 52428800 + artery { + transport = tcp + canonical.hostname = "127.0.0.1" + canonical.port = 0 + bind.hostname = "127.0.0.1" + bind.port = 0 + advanced { + maximum-frame-size = 52428800 + buffer-pool-size = 128 + maximum-large-frame-size = 52428800 } } } @@ -83,8 +87,12 @@ object AkkaConnectorActorConfig { s""" ${commonConf} pekko { - remote.netty.tcp.hostname = ${localHostname} - remote.netty.tcp.port = 0 + remote.artery { + canonical.hostname = ${localHostname} + canonical.port = 0 + bind.hostname = ${localHostname} + bind.port = 0 + } } """ @@ -92,8 +100,12 @@ object AkkaConnectorActorConfig { s""" ${commonConf} pekko { - remote.netty.tcp.hostname = ${localHostname} - remote.netty.tcp.port = ${localPort} + remote.artery { + canonical.hostname = ${localHostname} + canonical.port = ${localPort} + bind.hostname = ${localHostname} + bind.port = ${localPort} + } } """ @@ -101,8 +113,12 @@ object AkkaConnectorActorConfig { s""" ${commonConf} pekko { - remote.netty.tcp.hostname = ${remoteHostname} - remote.netty.tcp.port = ${remotePort} + remote.artery { + canonical.hostname = ${remoteHostname} + canonical.port = ${remotePort} + bind.hostname = ${remoteHostname} + bind.port = ${remotePort} + } } """ } diff --git a/obp-api/src/test/scala/code/setup/ServerSetup.scala b/obp-api/src/test/scala/code/setup/ServerSetup.scala index 176ccfcd29..f6acfc8c1a 100644 --- a/obp-api/src/test/scala/code/setup/ServerSetup.scala +++ b/obp-api/src/test/scala/code/setup/ServerSetup.scala @@ -60,6 +60,11 @@ trait ServerSetup extends FeatureSpec with SendServerRequests setPropsValues("berlin_group_mandatory_headers" -> "") setPropsValues("berlin_group_mandatory_header_consent" -> "") + // Set system properties to force Pekko to use random available ports + // This prevents conflicts when both RunWebApp and tests are running + System.setProperty("pekko.remote.artery.canonical.port", "0") + System.setProperty("pekko.remote.artery.bind.port", "0") + val server = TestServer def baseRequest = host(server.host, server.port) val secured = APIUtil.getPropsAsBoolValue("external.https", false)