Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9077427
fix: enforce UK consent check on aggregate AISP balances/transactions…
hongwei1 Jul 15, 2026
cb22cd7
Merge remote-tracking branch 'Simon/develop' into develop-obp
hongwei1 Jul 15, 2026
462e608
fix: stop background server launch from hanging callers via process s…
hongwei1 Jul 15, 2026
f26bd07
docs: remove stale Hydra references from UK v4.0.1 test comments
hongwei1 Jul 15, 2026
1163527
test(uk-open-banking): assert real 403 for the 3 checkUKConsent-gated…
hongwei1 Jul 15, 2026
adba45a
Merge remote-tracking branch 'origin/develop' into feature/uk401-cons…
hongwei1 Jul 15, 2026
4cd6035
Merge remote-tracking branch 'origin/develop' into develop-obp
hongwei1 Jul 15, 2026
f9f2cb6
Merge remote-tracking branch 'Hongwei/feature/uk401-consent-403-asser…
hongwei1 Jul 15, 2026
f10d8b6
fix: print the bound PORT from background build/run scripts
hongwei1 Jul 15, 2026
21e57e4
fix: put bankId before accountId in createTransactionsJsonNew params
hongwei1 Jul 17, 2026
94befc2
fix: bind UK consent permissions to real accounts on authorisation
hongwei1 Jul 18, 2026
2c138a2
fix: differentiate can_* permissions for UK and Berlin Group system v…
hongwei1 Jul 18, 2026
2aa2478
feat: add ReadTransactionsCredits UK Open Banking system view
hongwei1 Jul 18, 2026
961addb
docs: fix stale additional_system_views value list in props template
hongwei1 Jul 18, 2026
fdb91f7
docs: document the consent-layer vs view-layer permission boundary
hongwei1 Jul 18, 2026
c0fe7f1
fix(berlin-group): unblock consent SCA authorisation flow
hongwei1 Jul 18, 2026
e201365
test(berlin-group): cover the unclaimed-consent SCA happy path end to…
hongwei1 Jul 18, 2026
c653f60
fix: return spec four-letter status codes on UK v4.0.1 consent responses
hongwei1 Jul 18, 2026
39b9825
fix: make UK consent datetime fields optional, fix ISO-8601 parsing
hongwei1 Jul 18, 2026
051167e
fix: expire UK Open Banking consents past their ExpirationDateTime
hongwei1 Jul 18, 2026
ccba546
feat: allow re-authentication of a UK Open Banking consent
hongwei1 Jul 18, 2026
c6eae41
fix: allow client-credentials lodging of a UK account-access consent
hongwei1 Jul 18, 2026
fc02d40
feat: emit x-fapi-interaction-id on UK v4.0.1 responses
hongwei1 Jul 18, 2026
f7b58e8
feat: add jwks_uri field to Consumer for OIDC client key storage
hongwei1 Jul 18, 2026
2cbc37e
feat: expose client_certificate through the read-only OIDC clients view
hongwei1 Jul 18, 2026
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
17 changes: 13 additions & 4 deletions flushall_build_and_run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -155,15 +155,24 @@ JAVA_OPTS="--add-opens java.base/java.lang=ALL-UNNAMED \
RUNTIME_LOG=/tmp/obp-api.log

if [ "$RUN_BACKGROUND" = true ]; then
# Run in background with output to log file (tee'd to /tmp as well)
nohup java $JAVA_OPTS -jar obp-api/target/obp-api.jar > >(tee "$RUNTIME_LOG") 2>&1 &
# Run in background with output redirected straight to a log file. Do NOT
# use `> >(tee "$RUNTIME_LOG")` here: process substitution's tee inherits
# this script's own stdout, so a caller that captures this script's output
# via `out=$(./flushall_build_and_run.sh --background ...)` never sees EOF
# — the substitution hangs forever, since the server (and thus the tee
# process backing the substitution) never exits on its own.
nohup java $JAVA_OPTS -jar obp-api/target/obp-api.jar > "$RUNTIME_LOG" 2>&1 &
SERVER_PID=$!
# Report the port the server will actually bind (dev.port in props), so callers
# that capture this script's output (e.g. smoke_test.sh) can parse it out.
SERVER_PORT=$(grep -E '^dev.port=' obp-api/src/main/resources/props/default.props 2>/dev/null | cut -d= -f2)
echo "✓ HTTP4S server started in background"
echo " PID: $SERVER_PID"
echo " Log: http4s-server.log (also $RUNTIME_LOG)"
echo " PORT: ${SERVER_PORT:-8080}"
echo " Log: $RUNTIME_LOG"
echo ""
echo "To stop the server: kill $SERVER_PID"
echo "To view logs: tail -f http4s-server.log"
echo "To view logs: tail -f $RUNTIME_LOG"
else
# Run in foreground (Ctrl+C to stop). Also tee output to /tmp so it can be
# tailed from another terminal without taking over this one.
Expand Down
4 changes: 4 additions & 0 deletions flushall_fast_build_and_run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -324,8 +324,12 @@ if [ "$RUN_BACKGROUND" = true ]; then
# Run in background with output to log file
nohup java $JAVA_OPTS -jar obp-api/target/obp-api.jar > http4s-server.log 2>&1 &
SERVER_PID=$!
# Report the port the server will actually bind (dev.port in props), so callers
# that capture this script's output (e.g. smoke_test.sh) can parse it out.
SERVER_PORT=$(grep -E '^dev.port=' obp-api/src/main/resources/props/default.props 2>/dev/null | cut -d= -f2)
echo "✓ HTTP4S server started in background"
echo " PID: $SERVER_PID"
echo " PORT: ${SERVER_PORT:-8080}"
echo " Log: http4s-server.log"
echo ""
echo "To stop the server: kill $SERVER_PID"
Expand Down
3 changes: 3 additions & 0 deletions obp-api/src/main/resources/props/sample.props.template
Original file line number Diff line number Diff line change
Expand Up @@ -1275,8 +1275,11 @@ database_messages_scheduler_interval=3600
ReadBalances,\
ReadTransactionsBasic,\
ReadTransactionsDebits,\
ReadTransactionsCredits,\
ReadTransactionsDetail, \
ReadAccountsBerlinGroup, \
ReadBalancesBerlinGroup, \
ReadTransactionsBerlinGroup, \
InitiatePaymentsBerlinGroup
# -----------------------------------------------------------------------------

Expand Down
1 change: 1 addition & 0 deletions obp-api/src/main/scala/bootstrap/liftweb/Boot.scala
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ class Boot extends MdcLoggable {
SYSTEM_READ_BALANCES_VIEW_ID,
SYSTEM_READ_TRANSACTIONS_BASIC_VIEW_ID,
SYSTEM_READ_TRANSACTIONS_DEBITS_VIEW_ID,
SYSTEM_READ_TRANSACTIONS_CREDITS_VIEW_ID,
SYSTEM_READ_TRANSACTIONS_DETAIL_VIEW_ID,
SYSTEM_READ_ACCOUNTS_BERLIN_GROUP_VIEW_ID,
SYSTEM_READ_BALANCES_BERLIN_GROUP_VIEW_ID,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import cats.data.{Kleisli, OptionT}
import cats.effect.IO
import code.api.Constant
import code.api.UKOpenBanking.v3_1_0.JSONFactory_UKOpenBanking_310.ConsentPostBodyUKV310
import code.api.util.APIUtil.{EmptyBody, ResourceDoc, connectorEmptyResponse, mockedDataText, passesPsd2Aisp, unboxFullOrFail, DateWithDayFormat}
import code.api.util.APIUtil.{EmptyBody, ResourceDoc, connectorEmptyResponse, mockedDataText, passesPsd2Aisp, unboxFullOrFail, parseIso8601OrDayDate}
import code.api.util.ApiTag
import code.api.util.CustomJsonFormats
import code.api.util.ErrorMessages.{AuthenticatedUserIsRequired, ConsentNotFound, ConsentViewNotFund, UnknownError}
import code.api.util.ErrorMessages.{AuthenticatedUserIsRequired, ConsentNotFound, ConsentViewNotFund, InvalidJsonFormat, UnknownError}
import code.api.util.http4s.Http4sRequestAttributes.{EndpointHelpers, RequestOps}
import code.api.util.CallContext
import code.api.util.{ConsentJWT, JwtUtil}
import code.api.util.{ConsentJWT, JwtUtil, NewStyle}
import code.consent.Consents
import code.util.Helper.MdcLoggable
import com.github.dwickern.macros.NameOf.nameOf
Expand Down Expand Up @@ -47,24 +47,40 @@ object Http4sUKOBv310AccountAccess extends MdcLoggable {
EndpointHelpers.executeFutureCreated(req) {
implicit val cc: CallContext = req.callContext
for {
u <- cc.user.toOption match {
case Some(user) => Future.successful(user)
case None => Future.failed(new RuntimeException(AuthenticatedUserIsRequired))
}
// Client-credentials lodging: require some authentication (consumer or user) but not a
// PSU specifically; reject only a fully anonymous request. The PSU is bound later at
// authorise time. Mirrors the Berlin Group native consent flow and the v4.0.1 handler.
_ <- if (cc.user.isEmpty && cc.consumer.isEmpty)
Future.failed(new RuntimeException(AuthenticatedUserIsRequired))
else Future.successful(())
createdByUser = cc.user.toOption
consentJson <- Future.fromTry(scala.util.Try(
com.openbankproject.commons.util.JsonAliases.parse(cc.httpBody.getOrElse("{}")).extract[ConsentPostBodyUKV310]
))
// Separate step (not inlined into the saveUKConsent call below) so a bad date string
// fails here -> 400. NewStyle.function.tryons (not Future.fromTry) is required for
// that: ErrorResponseConverter only special-cases APIFailureNewStyle to preserve a set
// HTTP code -- tryons wraps failures that way, a bare Future.fromTry(Try(...)) doesn't
// and falls through to unknownErrorToResponse, i.e. 500.
(expirationDateTime, transactionFromDateTime, transactionToDateTime) <- NewStyle.function.tryons(
s"$InvalidJsonFormat The Json body should have valid ISO-8601 ExpirationDateTime/TransactionFromDateTime/TransactionToDateTime values ", 400, Some(cc)) {
(
consentJson.Data.ExpirationDateTime.map(parseIso8601OrDayDate),
consentJson.Data.TransactionFromDateTime.map(parseIso8601OrDayDate),
consentJson.Data.TransactionToDateTime.map(parseIso8601OrDayDate)
)
}
consumerId = cc.consumer.map(_.consumerId.get)
_ <- passesPsd2Aisp(Some(cc))
createdConsent <- Future(Consents.consentProvider.vend.saveUKConsent(
Some(u),
createdByUser,
bankId = None,
accountIds = None,
consumerId = consumerId,
permissions = consentJson.Data.Permissions,
expirationDateTime = DateWithDayFormat.parse(consentJson.Data.ExpirationDateTime),
transactionFromDateTime = DateWithDayFormat.parse(consentJson.Data.TransactionFromDateTime),
transactionToDateTime = DateWithDayFormat.parse(consentJson.Data.TransactionToDateTime),
expirationDateTime = expirationDateTime,
transactionFromDateTime = transactionFromDateTime,
transactionToDateTime = transactionToDateTime,
apiStandard = Some("UKOpenBanking"),
apiVersion = Some("3.1.0")
)) map { i => connectorEmptyResponse(i, Some(cc)) }
Expand All @@ -83,11 +99,11 @@ object Http4sUKOBv310AccountAccess extends MdcLoggable {
"Status" : "${createdConsent.status}",
"StatusUpdateDateTime" : "${createdConsent.statusUpdateDateTime}",
"CreationDateTime" : "${createdConsent.creationDateTime}",
"TransactionToDateTime" : "${consentJson.Data.TransactionToDateTime}",
"ExpirationDateTime" : "${consentJson.Data.ExpirationDateTime}",
"TransactionToDateTime" : "${consentJson.Data.TransactionToDateTime.getOrElse("")}",
"ExpirationDateTime" : "${consentJson.Data.ExpirationDateTime.getOrElse("")}",
"Permissions" : ${consentJson.Data.Permissions.mkString("[\"", "\",\"", "\"]")},
"ConsentId" : "${createdConsent.consentId}",
"TransactionFromDateTime" : "${consentJson.Data.TransactionFromDateTime}"
"TransactionFromDateTime" : "${consentJson.Data.TransactionFromDateTime.getOrElse("")}"
}
}""")
}
Expand Down Expand Up @@ -196,11 +212,11 @@ object Http4sUKOBv310AccountAccess extends MdcLoggable {
"Status" : "${consent.status}",
"StatusUpdateDateTime" : "${consent.statusUpdateDateTime}",
"CreationDateTime" : "${consent.creationDateTime}",
"TransactionToDateTime" : "${consent.transactionToDateTime}",
"ExpirationDateTime" : "${consent.expirationDateTime}",
"TransactionToDateTime" : "${Option(consent.transactionToDateTime).getOrElse("")}",
"ExpirationDateTime" : "${Option(consent.expirationDateTime).getOrElse("")}",
"Permissions" : ${consentViews.mkString("[\"", "\",\"", "\"]")},
"ConsentId" : "${consent.consentId}",
"TransactionFromDateTime" : "${consent.transactionFromDateTime}"
"TransactionFromDateTime" : "${Option(consent.transactionFromDateTime).getOrElse("")}"
}
}""")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,11 +204,13 @@ object JSONFactory_UKOpenBanking_310 extends CustomJsonFormats {
Links: LinksV310,
Risk: String
)
// The three datetimes are 0..1 (open-ended if absent) per the UK spec's OBReadConsent1 —
// shared by the v3.1.0 and v4.0.1 consent-creation handlers.
case class ConsentPostBodyDataUKV310(
TransactionToDateTime: String,
ExpirationDateTime: String,
TransactionToDateTime: Option[String],
ExpirationDateTime: Option[String],
Permissions: List[String],
TransactionFromDateTime: String
TransactionFromDateTime: Option[String]
)
case class ConsentPostBodyUKV310(
Data: ConsentPostBodyDataUKV310,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ package code.api.UKOpenBanking.v4_0_1

import cats.data.{Kleisli, OptionT}
import cats.effect._
import code.api.util.APIUtil
import code.api.util.APIUtil.ResourceDoc
import code.api.util.http4s.ResourceDocMiddleware
import code.util.Helper.MdcLoggable
import com.openbankproject.commons.util.ApiVersion
import org.http4s._
import org.typelevel.ci.CIString

import scala.collection.mutable.ArrayBuffer

Expand Down Expand Up @@ -46,5 +48,19 @@ object Http4sUKOBv401 extends MdcLoggable {
.orElse(Http4sUKOBv401Vrp.routes(req))
}

val wrappedRoutes: HttpRoutes[IO] = ResourceDocMiddleware.apply(resourceDocs)(allRoutes)
private val fapiInteractionIdHeader = CIString("x-fapi-interaction-id")

// FAPI: every response carries x-fapi-interaction-id -- echoed from the request if the TPP sent
// one (for end-to-end tracing), otherwise generated as a fresh UUID so tracing works even when
// the TPP omits it. Applied as an outer middleware so it covers every UK v4.0.1 response,
// including error responses produced inside ResourceDocMiddleware.
private def withFapiInteractionId(routes: HttpRoutes[IO]): HttpRoutes[IO] =
Kleisli[HttpF, Request[IO], Response[IO]] { req =>
val interactionId = req.headers.get(fapiInteractionIdHeader)
.map(_.head.value)
.getOrElse(APIUtil.generateUUID())
routes(req).map(_.putHeaders(Header.Raw(fapiInteractionIdHeader, interactionId)))
}

val wrappedRoutes: HttpRoutes[IO] = withFapiInteractionId(ResourceDocMiddleware.apply(resourceDocs)(allRoutes))
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import cats.effect.IO
import code.api.APIFailureNewStyle
import code.api.Constant
import code.api.UKOpenBanking.v3_1_0.JSONFactory_UKOpenBanking_310.ConsentPostBodyUKV310
import code.api.util.APIUtil.{EmptyBody, ResourceDoc, HTTPParam, connectorEmptyResponse, createQueriesByHttpParams, defaultBankId, fullBoxOrException, passesPsd2Aisp, unboxFull, unboxFullOrFail, DateWithDayFormat}
import code.api.util.APIUtil.{EmptyBody, ResourceDoc, HTTPParam, connectorEmptyResponse, createQueriesByHttpParams, defaultBankId, fullBoxOrException, passesPsd2Aisp, unboxFull, unboxFullOrFail, parseIso8601OrDayDate}
import code.api.util.ApiTag
import code.api.util.CallContext
import code.api.util.CustomJsonFormats
import code.api.util.ErrorMessages.{AuthenticatedUserIsRequired, ConsentNotFound, ConsentViewNotFund, UnknownError}
import code.api.util.ErrorMessages.{AuthenticatedUserIsRequired, ConsentNotFound, ConsentViewNotFund, InvalidJsonFormat, UnknownError}
import code.api.util.http4s.Http4sRequestAttributes.{EndpointHelpers, RequestOps}
import code.api.util.newstyle.ViewNewStyle
import code.api.util.{APIUtil, ConsentJWT, JwtUtil, NewStyle}
Expand Down Expand Up @@ -94,24 +94,43 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable {
EndpointHelpers.executeFutureCreated(req) {
implicit val cc: CallContext = req.callContext
for {
u <- cc.user.toOption match {
case Some(user) => Future.successful(user)
case None => Future.failed(new RuntimeException(AuthenticatedUserIsRequired))
}
// Spec Step 2: the TPP lodges the consent via a client-credentials grant -- authenticated
// as an app (consumer) but with no PSU yet. Require some authentication (a consumer or a
// user) but not a PSU specifically; reject only a fully anonymous request. The PSU is
// bound later at authorise time (mUserId stays null until then), mirroring the Berlin
// Group native consent flow (Http4sBGv13AIS.createConsent). A request with a real user
// (e.g. DirectLogin) still works -- createdByUser just carries it through.
_ <- if (cc.user.isEmpty && cc.consumer.isEmpty)
Future.failed(new RuntimeException(AuthenticatedUserIsRequired))
else Future.successful(())
createdByUser = cc.user.toOption
consentJson <- Future.fromTry(scala.util.Try(
JsonAliases.parse(cc.httpBody.getOrElse("{}")).extract[ConsentPostBodyUKV310]
))
// Separate step (not inlined into the saveUKConsent call below) so a bad date string
// fails here -> 400. NewStyle.function.tryons (not Future.fromTry) is required for
// that: ErrorResponseConverter only special-cases APIFailureNewStyle to preserve a set
// HTTP code -- tryons wraps failures that way, a bare Future.fromTry(Try(...)) doesn't
// and falls through to unknownErrorToResponse, i.e. 500.
(expirationDateTime, transactionFromDateTime, transactionToDateTime) <- NewStyle.function.tryons(
s"$InvalidJsonFormat The Json body should have valid ISO-8601 ExpirationDateTime/TransactionFromDateTime/TransactionToDateTime values ", 400, Some(cc)) {
(
consentJson.Data.ExpirationDateTime.map(parseIso8601OrDayDate),
consentJson.Data.TransactionFromDateTime.map(parseIso8601OrDayDate),
consentJson.Data.TransactionToDateTime.map(parseIso8601OrDayDate)
)
}
consumerId = cc.consumer.map(_.consumerId.get)
_ <- passesPsd2Aisp(Some(cc))
createdConsent <- Future(Consents.consentProvider.vend.saveUKConsent(
Some(u),
createdByUser,
bankId = None,
accountIds = None,
consumerId = consumerId,
permissions = consentJson.Data.Permissions,
expirationDateTime = DateWithDayFormat.parse(consentJson.Data.ExpirationDateTime),
transactionFromDateTime = DateWithDayFormat.parse(consentJson.Data.TransactionFromDateTime),
transactionToDateTime = DateWithDayFormat.parse(consentJson.Data.TransactionToDateTime),
expirationDateTime = expirationDateTime,
transactionFromDateTime = transactionFromDateTime,
transactionToDateTime = transactionToDateTime,
apiStandard = Some("UKOpenBanking"),
apiVersion = Some("4.0.1")
)) map { i => connectorEmptyResponse(i, Some(cc)) }
Expand Down Expand Up @@ -195,9 +214,9 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable {
status = consent.status,
statusUpdateDateTime = consent.statusUpdateDateTime.toString,
permissions = consentViews,
expirationDateTime = consent.expirationDateTime.toString,
transactionFromDateTime = consent.transactionFromDateTime.toString,
transactionToDateTime = consent.transactionToDateTime.toString,
expirationDateTime = Option(consent.expirationDateTime).map(_.toString),
transactionFromDateTime = Option(consent.transactionFromDateTime).map(_.toString),
transactionToDateTime = Option(consent.transactionToDateTime).map(_.toString),
selfPath = s"/aisp/account-access-consents/$consentId"
)
}
Expand Down Expand Up @@ -2236,7 +2255,7 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable {
transactions.map(_.id),
view.viewId,
Some(cc))
} yield JSONFactory_UKOpenBanking_401.createTransactionsJsonNew(account.bankId, transactions, moderatedAttributes, view)
} yield JSONFactory_UKOpenBanking_401.createTransactionsJsonNew(account.bankId, accountId.value, transactions, moderatedAttributes, view)
}
}
resourceDocs += ResourceDoc(
Expand Down Expand Up @@ -2305,6 +2324,8 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable {
case req @ GET -> `ukV401Prefix` / "aisp" / "balances" =>
EndpointHelpers.withUser(req) { (u, cc) =>
for {
_ <- NewStyle.function.checkUKConsent(u, Some(cc))
_ <- passesPsd2Aisp(Some(cc))
availablePrivateAccounts <- Views.views.vend.getPrivateBankAccountsFuture(u)
(accounts, _) <- NewStyle.function.getBankAccounts(availablePrivateAccounts, Some(cc))
} yield JSONFactory_UKOpenBanking_401.createBalancesJSON(accounts)
Expand Down Expand Up @@ -3446,6 +3467,8 @@ object Http4sUKOBv401AccountInfo extends MdcLoggable {
case req @ GET -> `ukV401Prefix` / "aisp" / "transactions" =>
EndpointHelpers.withUser(req) { (u, cc) =>
for {
_ <- NewStyle.function.checkUKConsent(u, Some(cc))
_ <- passesPsd2Aisp(Some(cc))
(bank, _) <- NewStyle.function.getBank(BankId(defaultBankId), Some(cc))
availablePrivateAccounts <- Views.views.vend.getPrivateBankAccountsFuture(u)
(accounts, _) <- NewStyle.function.getBankAccounts(availablePrivateAccounts, Some(cc))
Expand Down
Loading
Loading