From 281134075f517f2a9ff47a60262d9543c67a38a6 Mon Sep 17 00:00:00 2001 From: simonredfern Date: Sat, 14 Mar 2026 10:26:48 +0100 Subject: [PATCH 1/5] Bootstrap OIDC Operator Consumer to intro doc --- .../docs/introductory_system_documentation.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/obp-api/src/main/resources/docs/introductory_system_documentation.md b/obp-api/src/main/resources/docs/introductory_system_documentation.md index 487a0ae010..4907d99f55 100644 --- a/obp-api/src/main/resources/docs/introductory_system_documentation.md +++ b/obp-api/src/main/resources/docs/introductory_system_documentation.md @@ -2866,6 +2866,23 @@ super_admin_user_ids=uuid-1,uuid-2 # Then remove super_admin_user_ids from props ``` +**Bootstrap OIDC Operator Consumer:** + +OBP can bootstrap a Consumer (Application) for OBP-OIDC at startup. This allows OBP-OIDC to authenticate as an application (without a User) and manage consumers via the API, eliminating the need for direct database access. + +The bootstrap consumer is granted the following Scopes: `CanGetConsumers`, `CanCreateConsumer`, `CanVerifyOidcClient`, `CanGetOidcClient`. + +These endpoints use `authMode = UserOrApplication`, meaning they can be accessed either by a logged-in User with Entitlements, or by an Application using a Consumer Key with Scopes. + +```properties +# Bootstrap OIDC Operator Consumer +# Both values must be between 10 and 250 characters. +oidc_operator_consumer_key=your-consumer-key-here +oidc_operator_consumer_secret=your-consumer-secret-here +``` + +Note: If you use the Bootstrap OIDC Operator Consumer, you may not need the Bootstrap OIDC Operator User, depending on how OBP-OIDC implements its authentication. + **Checking User Entitlements:** ```bash From 0d8a60cef3129ae4f99a7b4c1d045510afc0a8d3 Mon Sep 17 00:00:00 2001 From: simonredfern Date: Sat, 14 Mar 2026 21:27:30 +0100 Subject: [PATCH 2/5] Make ConsentRequest.consumerId wider - and Consumer ID generation now: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - azp is a UUID → use it directly (as before) - azp is present but not a UUID → {actualValue}_ (fixes the Option.toString bug) - azp is missing → plain UUID (fixes the {None}_ prefix) --- obp-api/src/main/scala/code/api/OAuth2.scala | 6 +++++- obp-api/src/main/scala/code/consent/ConsentRequest.scala | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/obp-api/src/main/scala/code/api/OAuth2.scala b/obp-api/src/main/scala/code/api/OAuth2.scala index 6db773c921..1252712552 100644 --- a/obp-api/src/main/scala/code/api/OAuth2.scala +++ b/obp-api/src/main/scala/code/api/OAuth2.scala @@ -501,7 +501,11 @@ object OAuth2Login extends RestHelper with MdcLoggable { val sub = getClaim(name = "sub", jwtToken = jwtToken) val email = getClaim(name = "email", jwtToken = jwtToken) val name = getClaim(name = "name", jwtToken = jwtToken).orElse(description) - val consumerId = if(APIUtil.checkIfStringIsUUID(azp.getOrElse(""))) azp else Some(s"{$azp}_${APIUtil.generateUUID()}") + val consumerId = azp match { + case Some(value) if APIUtil.checkIfStringIsUUID(value) => azp + case Some(value) => Some(s"{$value}_${APIUtil.generateUUID()}") + case None => Some(APIUtil.generateUUID()) + } Consumers.consumers.vend.getOrCreateConsumer( consumerId = consumerId, // Use azp as consumer id if it is uuid value key = Some(Helpers.randomString(40).toLowerCase), diff --git a/obp-api/src/main/scala/code/consent/ConsentRequest.scala b/obp-api/src/main/scala/code/consent/ConsentRequest.scala index e307eeb078..a920d0c910 100644 --- a/obp-api/src/main/scala/code/consent/ConsentRequest.scala +++ b/obp-api/src/main/scala/code/consent/ConsentRequest.scala @@ -29,7 +29,7 @@ class ConsentRequest extends ConsentRequestTrait with LongKeyedMapper[ConsentReq //the following are the obp consent. object ConsentRequestId extends MappedUUID(this) object Payload extends MappedText(this) - object ConsumerId extends MappedUUID(this) { + object ConsumerId extends MappedString(this, 250) { override def defaultValue = null } From fbbff2562476217835a37554787a8f7acbe6cc0f Mon Sep 17 00:00:00 2001 From: simonredfern Date: Sat, 14 Mar 2026 21:56:27 +0100 Subject: [PATCH 3/5] MigrationOfConsentRequestConsumerIdFieldLength - make longer to match consumer consumer_id --- .../code/api/util/migration/Migration.scala | 8 +++ ...fConsentRequestConsumerIdFieldLength.scala | 54 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 obp-api/src/main/scala/code/api/util/migration/MigrationOfConsentRequestConsumerIdFieldLength.scala diff --git a/obp-api/src/main/scala/code/api/util/migration/Migration.scala b/obp-api/src/main/scala/code/api/util/migration/Migration.scala index 84cef63c05..361ee405e2 100644 --- a/obp-api/src/main/scala/code/api/util/migration/Migration.scala +++ b/obp-api/src/main/scala/code/api/util/migration/Migration.scala @@ -107,6 +107,7 @@ object Migration extends MdcLoggable { addUniqueIndexOnResourceUserUserId() addIndexOnMappedMetricUserId() alterRoleNameLength() + alterConsentRequestColumnConsumerIdLength() } private def dummyScript(): Boolean = { @@ -553,6 +554,13 @@ object Migration extends MdcLoggable { MigrationOfRoleNameFieldLength.alterRoleNameLength(name) } } + + private def alterConsentRequestColumnConsumerIdLength(): Boolean = { + val name = nameOf(alterConsentRequestColumnConsumerIdLength) + runOnce(name) { + MigrationOfConsentRequestConsumerIdFieldLength.alterColumnConsumerIdLength(name) + } + } } /** diff --git a/obp-api/src/main/scala/code/api/util/migration/MigrationOfConsentRequestConsumerIdFieldLength.scala b/obp-api/src/main/scala/code/api/util/migration/MigrationOfConsentRequestConsumerIdFieldLength.scala new file mode 100644 index 0000000000..3e2453cd7d --- /dev/null +++ b/obp-api/src/main/scala/code/api/util/migration/MigrationOfConsentRequestConsumerIdFieldLength.scala @@ -0,0 +1,54 @@ +package code.api.util.migration + +import code.api.util.APIUtil +import code.api.util.migration.Migration.{DbFunction, saveLog} +import code.consent.ConsentRequest +import net.liftweb.common.Full +import net.liftweb.mapper.Schemifier + +object MigrationOfConsentRequestConsumerIdFieldLength { + + def alterColumnConsumerIdLength(name: String): Boolean = { + DbFunction.tableExists(ConsentRequest) match { + case true => + val startDate = System.currentTimeMillis() + val commitId: String = APIUtil.gitCommit + var isSuccessful = false + + val executedSql = + DbFunction.maybeWrite(true, Schemifier.infoF _) { + APIUtil.getPropsValue("db.driver") match { + case Full(dbDriver) if dbDriver.contains("com.microsoft.sqlserver.jdbc.SQLServerDriver") => + () => + """ + |ALTER TABLE consentrequest ALTER COLUMN consumerid varchar(250); + |""".stripMargin + case _ => + () => + """ + |ALTER TABLE consentrequest ALTER COLUMN consumerid TYPE character varying(250); + |""".stripMargin + } + } + + val endDate = System.currentTimeMillis() + val comment: String = + s"""Executed SQL: + |$executedSql + |""".stripMargin + isSuccessful = true + saveLog(name, commitId, isSuccessful, startDate, endDate, comment) + isSuccessful + + case false => + val startDate = System.currentTimeMillis() + val commitId: String = APIUtil.gitCommit + val isSuccessful = false + val endDate = System.currentTimeMillis() + val comment: String = + s"""${ConsentRequest._dbTableNameLC} table does not exist""".stripMargin + saveLog(name, commitId, isSuccessful, startDate, endDate, comment) + isSuccessful + } + } +} From 3568961124bd7e8d0c09a829ac275b653e05f0c7 Mon Sep 17 00:00:00 2001 From: simonredfern Date: Mon, 16 Mar 2026 07:32:18 +0100 Subject: [PATCH 4/5] Migration of consumerid in consent to 250. Port of Hola to 48123. Better format of consumerId from azp --- .../docs/introductory_system_documentation.md | 6 +-- obp-api/src/main/scala/code/api/OAuth2.scala | 2 +- .../SwaggerDefinitionsJSON.scala | 2 +- .../main/scala/code/api/util/APIUtil.scala | 2 +- .../code/api/util/migration/Migration.scala | 8 +++ .../migration/MigrationOfMappedConsent.scala | 50 +++++++++++++++++++ .../scala/code/consent/MappedConsent.scala | 2 +- obp-api/src/main/scala/code/model/OAuth.scala | 14 ++++-- 8 files changed, 76 insertions(+), 10 deletions(-) diff --git a/obp-api/src/main/resources/docs/introductory_system_documentation.md b/obp-api/src/main/resources/docs/introductory_system_documentation.md index 4907d99f55..c1e99af082 100644 --- a/obp-api/src/main/resources/docs/introductory_system_documentation.md +++ b/obp-api/src/main/resources/docs/introductory_system_documentation.md @@ -900,7 +900,7 @@ obp.base_url=https://api.example.com # Client credentials (from OBP consumer registration) oauth2.client_id=your-client-id -oauth2.redirect_uri=http://localhost:8087/callback +oauth2.redirect_uri=http://localhost:48123/callback oauth2.client_scope=ReadAccountsDetail ReadBalances ReadTransactionsDetail # mTLS (if required) @@ -917,14 +917,14 @@ mvn clean package # Run locally java -jar target/obp-hola-app-0.0.29-SNAPSHOT.jar -# Access at http://localhost:8087 +# Access at http://localhost:48123 ``` **Docker Deployment:** ```bash docker build -t obp-hola . -docker run -p 8087:8087 \ +docker run -p 48123:48123 \ -e OAUTH2_PUBLIC_URL=https://oauth2.example.com \ -e OBP_BASE_URL=https://api.example.com \ obp-hola diff --git a/obp-api/src/main/scala/code/api/OAuth2.scala b/obp-api/src/main/scala/code/api/OAuth2.scala index 1252712552..9cee12f436 100644 --- a/obp-api/src/main/scala/code/api/OAuth2.scala +++ b/obp-api/src/main/scala/code/api/OAuth2.scala @@ -503,7 +503,7 @@ object OAuth2Login extends RestHelper with MdcLoggable { val name = getClaim(name = "name", jwtToken = jwtToken).orElse(description) val consumerId = azp match { case Some(value) if APIUtil.checkIfStringIsUUID(value) => azp - case Some(value) => Some(s"{$value}_${APIUtil.generateUUID()}") + case Some(value) => Some(s"${value}_${APIUtil.generateUUID()}") case None => Some(APIUtil.generateUUID()) } Consumers.consumers.vend.getOrCreateConsumer( 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 44918b7ffb..cf0faf6245 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 @@ -6208,7 +6208,7 @@ object SwaggerDefinitionsJSON { ConfigPropJsonV600("public_obp_sandbox_populator_url", "http://localhost:5178"), ConfigPropJsonV600("public_obp_oidc_url", "http://localhost:9000"), ConfigPropJsonV600("public_keycloak_url", "http://localhost:7787"), - ConfigPropJsonV600("public_obp_hola_url", "http://localhost:8087"), + ConfigPropJsonV600("public_obp_hola_url", "http://localhost:48123"), ConfigPropJsonV600("public_obp_mcp_url", "http://localhost:9100"), ConfigPropJsonV600("public_obp_opey_url", "http://localhost:5000") ) diff --git a/obp-api/src/main/scala/code/api/util/APIUtil.scala b/obp-api/src/main/scala/code/api/util/APIUtil.scala index 3bf6cac629..536a12d18e 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -4093,7 +4093,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ "public_obp_sandbox_populator_url" -> getPropsValue("public_obp_sandbox_populator_url").openOr("http://localhost:5178"), "public_obp_oidc_url" -> getPropsValue("public_obp_oidc_url").openOr("http://localhost:9000"), "public_keycloak_url" -> getPropsValue("public_keycloak_url").openOr("http://localhost:7787"), - "public_obp_hola_url" -> getPropsValue("public_obp_hola_url").openOr("http://localhost:8087"), + "public_obp_hola_url" -> getPropsValue("public_obp_hola_url").openOr("http://localhost:48123"), "public_obp_mcp_url" -> getPropsValue("public_obp_mcp_url").openOr("http://localhost:9100"), "public_obp_opey_url" -> getPropsValue("public_obp_opey_url").openOr("http://localhost:5000"), "public_rabbit_cats_adapter_url" -> getPropsValue("public_rabbit_cats_adapter_url").openOr("http://localhost:8089") diff --git a/obp-api/src/main/scala/code/api/util/migration/Migration.scala b/obp-api/src/main/scala/code/api/util/migration/Migration.scala index 361ee405e2..34527df59f 100644 --- a/obp-api/src/main/scala/code/api/util/migration/Migration.scala +++ b/obp-api/src/main/scala/code/api/util/migration/Migration.scala @@ -108,6 +108,7 @@ object Migration extends MdcLoggable { addIndexOnMappedMetricUserId() alterRoleNameLength() alterConsentRequestColumnConsumerIdLength() + alterMappedConsentColumnConsumerIdLength() } private def dummyScript(): Boolean = { @@ -561,6 +562,13 @@ object Migration extends MdcLoggable { MigrationOfConsentRequestConsumerIdFieldLength.alterColumnConsumerIdLength(name) } } + + private def alterMappedConsentColumnConsumerIdLength(): Boolean = { + val name = nameOf(alterMappedConsentColumnConsumerIdLength) + runOnce(name) { + MigrationOfMappedConsent.alterColumnConsumerIdLength(name) + } + } } /** diff --git a/obp-api/src/main/scala/code/api/util/migration/MigrationOfMappedConsent.scala b/obp-api/src/main/scala/code/api/util/migration/MigrationOfMappedConsent.scala index 412b279320..ecb043b779 100644 --- a/obp-api/src/main/scala/code/api/util/migration/MigrationOfMappedConsent.scala +++ b/obp-api/src/main/scala/code/api/util/migration/MigrationOfMappedConsent.scala @@ -102,6 +102,56 @@ object MigrationOfMappedConsent { isSuccessful } } + // The mConsumerId column was originally MappedUUID (varchar(36)), but Consumer.consumerId + // is MappedString(250) and can hold composite IDs like "{azp_value}_UUID" generated + // by OAuth2.getOrCreateConsumer when the azp claim is not a UUID. + // This migration widens mConsumerId to match the Consumer model. + def alterColumnConsumerIdLength(name: String): Boolean = { + DbFunction.tableExists(MappedConsent) match { + case true => + val startDate = System.currentTimeMillis() + val commitId: String = APIUtil.gitCommit + var isSuccessful = false + + val executedSql = + DbFunction.maybeWrite(true, Schemifier.infoF _) { + APIUtil.getPropsValue("db.driver") match { + case Full(dbDriver) if dbDriver.contains("com.microsoft.sqlserver.jdbc.SQLServerDriver") => + () => + """ALTER TABLE mappedconsent ALTER COLUMN mconsumerid varchar(250); + |""".stripMargin + case Full(dbDriver) if dbDriver.contains("com.mysql.cj.jdbc.Driver") => // MySQL + () => + """ALTER TABLE mappedconsent MODIFY COLUMN mconsumerid varchar(250); + |""".stripMargin + case _ => + () => + """ALTER TABLE mappedconsent ALTER COLUMN mconsumerid TYPE character varying(250); + |""".stripMargin + } + } + + val endDate = System.currentTimeMillis() + val comment: String = + s"""Executed SQL: + |$executedSql + |""".stripMargin + isSuccessful = true + saveLog(name, commitId, isSuccessful, startDate, endDate, comment) + isSuccessful + + case false => + val startDate = System.currentTimeMillis() + val commitId: String = APIUtil.gitCommit + val isSuccessful = false + val endDate = System.currentTimeMillis() + val comment: String = + s"""${MappedConsent._dbTableNameLC} table does not exist""".stripMargin + saveLog(name, commitId, isSuccessful, startDate, endDate, comment) + isSuccessful + } + } + def alterColumnStatus(name: String): Boolean = { DbFunction.tableExists(MappedConsent) match { case true => diff --git a/obp-api/src/main/scala/code/consent/MappedConsent.scala b/obp-api/src/main/scala/code/consent/MappedConsent.scala index 1a53050745..6bb0e692c7 100644 --- a/obp-api/src/main/scala/code/consent/MappedConsent.scala +++ b/obp-api/src/main/scala/code/consent/MappedConsent.scala @@ -400,7 +400,7 @@ class MappedConsent extends ConsentTrait with LongKeyedMapper[MappedConsent] wit override def defaultValue = BCrypt.gensalt() } object mJsonWebToken extends MappedText(this) - object mConsumerId extends MappedUUID(this) { + object mConsumerId extends MappedString(this, 250) { override def defaultValue = null } object mConsentRequestId extends MappedUUID(this) { diff --git a/obp-api/src/main/scala/code/model/OAuth.scala b/obp-api/src/main/scala/code/model/OAuth.scala index d132c2fd44..ea00a31930 100644 --- a/obp-api/src/main/scala/code/model/OAuth.scala +++ b/obp-api/src/main/scala/code/model/OAuth.scala @@ -510,9 +510,17 @@ class Consumer extends LongKeyedMapper[Consumer] with CreatedUpdated{ def getSingleton = Consumer def primaryKeyField = id - //Note: we have two id here for Consumer. id is the primaryKeyField, we used it as the CONSUMER_ID in api level for a long time. - //But from `a4222f9824fcac039e7968f4abcd009fa3918d4a` 2017-07-07 we introduced the consumerId here. It is confused now - //For now consumerId is only used in Gateway Login, all other cases, we should use the id instead `consumerId`. + // Note: There are two IDs on Consumer. + // `id` is the Long primary key (MappedLongIndex). + // `consumerId` is the UUID-based string identifier exposed externally as consumer_id in the API. + // + // consumerId is 250 chars to accommodate: + // - Standard UUIDs (36 chars) — the default + // - Gateway Login external app_id values (variable length) + // - OAuth2 composite IDs in format "azp_UUID" created by OAuth2.getOrCreateConsumer (up to ~77 chars) + // + // WARNING: Do not increase this length. Other tables (e.g. MappedConsent.mConsumerId) store + // copies of this value. object id extends MappedLongIndex(this) object consumerId extends MappedString(this, 250) { // Introduced to cover gateway login functionality override def defaultValue = APIUtil.generateUUID() From e13e300e9ae0a89ba956d7d4a0c14ddbbb8fdf01 Mon Sep 17 00:00:00 2001 From: simonredfern Date: Mon, 16 Mar 2026 07:47:45 +0100 Subject: [PATCH 5/5] docfix: consents.sca.enabled=true --- obp-api/src/main/resources/props/sample.props.template | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 636ed4225a..699e92a6ec 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -1151,7 +1151,7 @@ database_messages_scheduler_interval=3600 # consumer_validation_method_for_consent=CONSUMER_CERTIFICATE # # consents.max_time_to_live=3600 -# In case isn't defined default value is "false" +# In case isn't defined default value is "true" # consents.sca.enabled=true # ---------------------------------------------------------