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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion obp-api/src/main/resources/props/sample.props.template
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ---------------------------------------------------------

Expand Down
6 changes: 5 additions & 1 deletion obp-api/src/main/scala/code/api/OAuth2.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
)
Expand Down
2 changes: 1 addition & 1 deletion obp-api/src/main/scala/code/api/util/APIUtil.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
16 changes: 16 additions & 0 deletions obp-api/src/main/scala/code/api/util/migration/Migration.scala
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ object Migration extends MdcLoggable {
addUniqueIndexOnResourceUserUserId()
addIndexOnMappedMetricUserId()
alterRoleNameLength()
alterConsentRequestColumnConsumerIdLength()
alterMappedConsentColumnConsumerIdLength()
}

private def dummyScript(): Boolean = {
Expand Down Expand Up @@ -553,6 +555,20 @@ object Migration extends MdcLoggable {
MigrationOfRoleNameFieldLength.alterRoleNameLength(name)
}
}

private def alterConsentRequestColumnConsumerIdLength(): Boolean = {
val name = nameOf(alterConsentRequestColumnConsumerIdLength)
runOnce(name) {
MigrationOfConsentRequestConsumerIdFieldLength.alterColumnConsumerIdLength(name)
}
}

private def alterMappedConsentColumnConsumerIdLength(): Boolean = {
val name = nameOf(alterMappedConsentColumnConsumerIdLength)
runOnce(name) {
MigrationOfMappedConsent.alterColumnConsumerIdLength(name)
}
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
APIUtil.getPropsValue("db.driver") match {
case Full(dbDriver) if dbDriver.contains("com.microsoft.sqlserver.jdbc.SQLServerDriver") =>
() => "ALTER TABLE mappedconsent ALTER COLUMN mjsonwebtoken text;"
case Full(dbDriver) if dbDriver.contains("com.mysql.cj.jdbc.Driver") => // MySQL

Check failure on line 31 in obp-api/src/main/scala/code/api/util/migration/MigrationOfMappedConsent.scala

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "com.mysql.cj.jdbc.Driver" 3 times.

See more on https://sonarcloud.io/project/issues?id=OpenBankProject_OBP-API&issues=AZz14P9wePk3cDFqf_mi&open=AZz14P9wePk3cDFqf_mi&pullRequest=2733
() => "ALTER TABLE mappedconsent MODIFY COLUMN mjsonwebtoken TEXT;"
case _ =>
() => "ALTER TABLE mappedconsent ALTER COLUMN mjsonwebtoken type text;"
Expand Down Expand Up @@ -102,6 +102,56 @@
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 =>
Expand Down
2 changes: 1 addition & 1 deletion obp-api/src/main/scala/code/consent/ConsentRequest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
2 changes: 1 addition & 1 deletion obp-api/src/main/scala/code/consent/MappedConsent.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
14 changes: 11 additions & 3 deletions obp-api/src/main/scala/code/model/OAuth.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading