Skip to content

Commit ad5e720

Browse files
Merge remote-tracking branch 'upstream/develop' into develop
2 parents ea5aff5 + f32fac4 commit ad5e720

38 files changed

Lines changed: 855 additions & 73 deletions

obp-api/src/main/protobuf/metrics_stream.proto

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ message StreamMetricsRequest {
1111
string url_substring = 4;
1212
string implemented_by_partial_function = 5;
1313
string app_name = 6;
14+
string consent_reference_id = 7;
1415
}
1516

1617
// Per-REST-call metric record, mirrors APIMetrics.saveMetric args and
@@ -42,6 +43,9 @@ message MetricEvent {
4243
string api_instance_id = 16;
4344
// OBP operation id, e.g. "OBPv6.0.0-getBanks". Matches MetricJsonV600.operation_id.
4445
string operation_id = 17;
46+
// Reference id of the consent (if any) that authorised the request.
47+
// Mirrors MetricJsonV600.consent_reference_id (REST v6.0.0+).
48+
string consent_reference_id = 18;
4549
}
4650

4751
// Live tail of API metrics as they are written.

obp-api/src/main/resources/props/sample.props.template

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1150,7 +1150,8 @@ database_messages_scheduler_interval=3600
11501150
# Possibile values are: CONSUMER_CERTIFICATE, CONSUMER_KEY_VALUE, NONE
11511151
# consumer_validation_method_for_consent=CONSUMER_CERTIFICATE
11521152
#
1153-
# consents.max_time_to_live=3600
1153+
# Maximum allowed time_to_live (in seconds) for a consent. Default is 7776000 (90 days), matching PSD2 AIS / UK Open Banking.
1154+
# consents.max_time_to_live=7776000
11541155
# In case isn't defined default value is "true"
11551156
# consents.sca.enabled=true
11561157
# ---------------------------------------------------------

obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3193,7 +3193,8 @@ object SwaggerDefinitionsJSON {
31933193
response_body = json.parse("""{"code":401,"message":"OBP-20001: User not logged in. Authentication is required!"}"""),
31943194
status_code = 401,
31953195
operation_id = "OBPv4.0.0-getBanks",
3196-
api_instance_id = "obp_node_a"
3196+
api_instance_id = "obp_node_a",
3197+
consent_reference_id = Some(ExampleValue.consentReferenceIdExample.value)
31973198
)
31983199
lazy val metricsJsonV600 = MetricsJsonV600(
31993200
metrics = List(metricJsonV600)

obp-api/src/main/scala/code/api/constant/constant.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,10 @@ object Constant extends MdcLoggable {
306306
def RATE_LIMIT_ACTIVE_PREFIX: String = getVersionedCachePrefix(RL_ACTIVE_NAMESPACE)
307307
final val RATE_LIMIT_ACTIVE_CACHE_TTL: Int = APIUtil.getPropsValue("rateLimitActive.cache.ttl.seconds", "3600").toInt
308308

309+
// Default max time_to_live for consents, in seconds. 90 days — aligns with PSD2 AIS / UK Open Banking.
310+
// Used as the fallback when the `consents.max_time_to_live` prop is unset.
311+
final val DEFAULT_CONSENT_TTL: Int = 7776000
312+
309313
// Connector Cache Prefixes (with global namespace and versioning)
310314
def CONNECTOR_PREFIX: String = getVersionedCachePrefix(CONNECTOR_NAMESPACE)
311315

obp-api/src/main/scala/code/api/util/APIUtil.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1199,6 +1199,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
11991199
case "azp" => Full(OBPAzp(values.head))
12001200
case "iss" => Full(OBPIss(values.head))
12011201
case "consent_id" => Full(OBPConsentId(values.head))
1202+
case "consent_reference_id" => Full(OBPConsentReferenceId(values.head))
12021203
case "user_id" => Full(OBPUserId(values.head))
12031204
case "provider_provider_id" => Full(ProviderProviderId(values.head))
12041205
case "bank_id" => Full(OBPBankId(values.head))
@@ -1333,6 +1334,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
13331334
val iss = getHttpRequestUrlParam(httpRequestUrl,"iss")
13341335
val azp = getHttpRequestUrlParam(httpRequestUrl,"azp")
13351336
val consentId = getHttpRequestUrlParam(httpRequestUrl,"consent_id")
1337+
val consentReferenceId = getHttpRequestUrlParam(httpRequestUrl,"consent_reference_id")
13361338
val userId = getHttpRequestUrlParam(httpRequestUrl, "user_id")
13371339
val providerProviderId = getHttpRequestUrlParam(httpRequestUrl, "provider_provider_id")
13381340
val bankId = getHttpRequestUrlParam(httpRequestUrl, "bank_id")
@@ -1368,7 +1370,7 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{
13681370

13691371
Full(List(
13701372
HTTPParam("sort_by",sortBy), HTTPParam("sort_direction",sortDirection), HTTPParam("from_date",fromDate), HTTPParam("to_date", toDate), HTTPParam("limit",limit), HTTPParam("offset",offset),
1371-
HTTPParam("anon", anon), HTTPParam("status", status), HTTPParam("consumer_id", consumerId), HTTPParam("azp", azp), HTTPParam("iss", iss), HTTPParam("consent_id", consentId), HTTPParam("user_id", userId), HTTPParam("provider_provider_id", providerProviderId), HTTPParam("url", url), HTTPParam("app_name", appName),
1373+
HTTPParam("anon", anon), HTTPParam("status", status), HTTPParam("consumer_id", consumerId), HTTPParam("azp", azp), HTTPParam("iss", iss), HTTPParam("consent_id", consentId), HTTPParam("consent_reference_id", consentReferenceId), HTTPParam("user_id", userId), HTTPParam("provider_provider_id", providerProviderId), HTTPParam("url", url), HTTPParam("app_name", appName),
13721374
HTTPParam("implemented_by_partial_function",implementedByPartialFunction), HTTPParam("implemented_in_version",implementedInVersion), HTTPParam("verb", verb),
13731375
HTTPParam("correlation_id", correlationId), HTTPParam("duration", duration), HTTPParam("exclude_app_names", excludeAppNames),
13741376
HTTPParam("exclude_url_patterns", excludeUrlPattern),HTTPParam("exclude_implemented_by_partial_functions", excludeImplementedByPartialfunctions),

obp-api/src/main/scala/code/api/util/ApiSession.scala

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ case class CallContext(
5858
bank: Option[Bank] = None,
5959
bankAccount: Option[BankAccount] = None,
6060
view: Option[View] = None,
61-
counterparty: Option[CounterpartyTrait] = None
61+
counterparty: Option[CounterpartyTrait] = None,
62+
// Set when the request is authenticated via a consent. Persisted on metric rows for search/audit.
63+
consentReferenceId: Option[String] = None
6264
) extends MdcLoggable {
6365
override def toString: String = SecureLogging.maskSensitive(
6466
s"${this.getClass.getSimpleName}(${this.productIterator.mkString(", ")})"
@@ -144,7 +146,8 @@ case class CallContext(
144146
xRateLimitRemaining = this.xRateLimitRemaining,
145147
xRateLimitReset = this.xRateLimitReset,
146148
paginationOffset = this.paginationOffset,
147-
paginationLimit = this.paginationLimit
149+
paginationLimit = this.paginationLimit,
150+
consentReferenceId = this.consentReferenceId
148151
)
149152
}
150153

@@ -210,7 +213,8 @@ case class CallContextLight(gatewayLoginRequestPayload: Option[PayloadOfJwtJSON]
210213
xRateLimitRemaining : Long = -1,
211214
xRateLimitReset : Long = -1,
212215
paginationOffset : Option[String] = None,
213-
paginationLimit : Option[String] = None
216+
paginationLimit : Option[String] = None,
217+
consentReferenceId: Option[String] = None
214218
)
215219

216220
trait LoginParam

obp-api/src/main/scala/code/api/util/ConsentUtil.scala

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -451,14 +451,20 @@ object Consent extends MdcLoggable {
451451
def applyConsentRules(consent: ConsentJWT): Future[(Box[User], Option[CallContext])] = {
452452
val temp = callContext
453453
// updated context if createdByUserId is present
454-
val cc = if (consent.createdByUserId.nonEmpty) {
454+
val ccWithOnBehalf = if (consent.createdByUserId.nonEmpty) {
455455
val onBehalfOfUser = Users.users.vend.getUserByUserId(consent.createdByUserId)
456456
temp.copy(onBehalfOfUser = onBehalfOfUser.toOption)
457457
} else {
458458
temp
459459
}
460+
// Stamp the consent_reference_id on the CallContext so the metric writer can record it.
461+
val cc = Consents.consentProvider.vend.getConsentByConsentId(consent.jti) match {
462+
case Full(mc) => ccWithOnBehalf.copy(consentReferenceId = Some(mc.consentReferenceId))
463+
case _ => ccWithOnBehalf
464+
}
460465
if (cc.onBehalfOfUser.nonEmpty &&
461466
APIUtil.getPropsAsBoolValue(nameOfProperty = "experimental_become_user_that_created_consent", defaultValue = false)) {
467+
logger.warn("WARNING: experimental_become_user_that_created_consent is DEPRECATED and will be removed soon. Please unset this property.")
462468
logger.info("experimental_become_user_that_created_consent = true")
463469
logger.info(s"${cc.onBehalfOfUser.map(_.userId).getOrElse("")} is logged on instead of Consent user")
464470
Future(cc.onBehalfOfUser, Some(cc)) // Just propagate on behalf of user back
@@ -552,7 +558,11 @@ object Consent extends MdcLoggable {
552558
implicit val dateFormats = CustomJsonFormats.formats
553559

554560
def applyConsentRules(consent: ConsentJWT, callContext: CallContext): Future[(Box[User], Option[CallContext])] = {
555-
val cc = callContext
561+
// Stamp the consent_reference_id on the CallContext so the metric writer can record it.
562+
val cc = Consents.consentProvider.vend.getConsentByConsentId(consent.jti) match {
563+
case Full(mc) => callContext.copy(consentReferenceId = Some(mc.consentReferenceId))
564+
case _ => callContext
565+
}
556566
// 1. Get or Create a User
557567
getOrCreateUser(consent.sub, consent.iss, Some(consent.toConsent().consentId), None, None) map {
558568
case (Full(user), newUser) =>

obp-api/src/main/scala/code/api/util/ExampleValue.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1630,8 +1630,8 @@ object ExampleValue {
16301630
lazy val directDebitIdExample = ConnectorField(NoExampleProvided,NoDescriptionProvided)
16311631
glossaryItems += makeGlossaryItem("direct_debit_id", directDebitIdExample)
16321632

1633-
lazy val consentReferenceIdExample = ConnectorField("123456" ,NoDescriptionProvided)
1634-
glossaryItems += makeGlossaryItem("consent_id", consentReferenceIdExample)
1633+
lazy val consentReferenceIdExample = ConnectorField("fd13b9af-4f74-4d52-a7f1-7c2c12f3aa11" ,NoDescriptionProvided)
1634+
glossaryItems += makeGlossaryItem("consent_reference_id", consentReferenceIdExample)
16351635

16361636
lazy val consentIdExample = ConnectorField("9d429899-24f5-42c8-8565-943ffa6a7947",NoDescriptionProvided)
16371637
glossaryItems += makeGlossaryItem("consent_id", consentIdExample)

obp-api/src/main/scala/code/api/util/Glossary.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5672,7 +5672,7 @@ object Glossary extends MdcLoggable {
56725672
|For onward calls to OBP-API, `OBP_AUTHORIZATION_VIA` selects:
56735673
|
56745674
|- **`oauth`** — pulls the access token from the MCP request context and sends `Authorization: Bearer ...`.
5675-
|- **`consent`** — if the endpoint declares any required roles and no `Consent-JWT` is supplied, the tool returns a `consent_required` payload listing the required roles and bank scope, so the client can elicit user approval and come back with a `Consent-JWT` header. Public / no-role endpoints skip this and call straight through.
5675+
|- **`consent`** — the default mode for user-facing deployments. `call_obp_api` requires a `Consent-JWT` for **every** endpoint except a small allowlist of genuinely public ones (`GET /root`, the bank directory `/banks` and `/banks/{BANK_ID}`, glossary, resource-docs, API metadata). For any other endpoint called without a `Consent-JWT`, the tool returns a `consent_required` payload required roles, bank / account / view scope, and `requires_view_access` / `is_user_scoped` flags — so the client can build the right consent and retry with a `Consent-JWT` header. Consent is required **by default**, not only for role-gated endpoints, because many identity-bound endpoints (`/users/current`, `/my/*`, account-access-via-view endpoints) declare no roles yet still need the caller's identity — a role-only gate would call them unauthenticated. The allowlist is deliberately conservative: a wrongly-excluded endpoint costs only an extra prompt, whereas wrongly skipping consent fails silently.
56765676
|- **`none`** — calls OBP unauthenticated (only useful for genuinely public endpoints).
56775677
|
56785678
|This means the consent flow is enforced at the MCP layer, not just at OBP-API: an agent cannot accidentally call a privileged endpoint without explicit user consent.

obp-api/src/main/scala/code/api/util/OBPParam.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ case class OBPSortBy(value: String) extends OBPQueryParam
3030
case class OBPAzp(value: String) extends OBPQueryParam
3131
case class OBPIss(value: String) extends OBPQueryParam
3232
case class OBPConsentId(value: String) extends OBPQueryParam
33+
case class OBPConsentReferenceId(value: String) extends OBPQueryParam
3334
case class OBPUserId(value: String) extends OBPQueryParam
3435
case class ProviderProviderId(value: String) extends OBPQueryParam
3536
case class OBPStatus(value: String) extends OBPQueryParam

0 commit comments

Comments
 (0)