From e94828be6172626a1cdd75647f5b174f9e0a5f29 Mon Sep 17 00:00:00 2001 From: simonredfern Date: Tue, 14 Jul 2026 14:55:08 +0200 Subject: [PATCH 1/3] Updating roadmap --- completed_developments.md | 12 +++++- roadmap.md | 91 ++++++++++++++++++++++++++++----------- 2 files changed, 76 insertions(+), 27 deletions(-) diff --git a/completed_developments.md b/completed_developments.md index e6b8a46a9b..fbb1b34fb5 100644 --- a/completed_developments.md +++ b/completed_developments.md @@ -1,6 +1,16 @@ ## Completed developments (most recent first) -(a non exhaustive summary!) +This is a curated log of major engineering milestones, deliberately **not** a complete history. The authoritative record of shipped API functionality is the Resource Docs / API Explorer, the glossary and the README — this file exists for the work those structurally cannot show: re-platforms, dependency removals, performance and architecture changes that alter no API signature. + +### Lift Web → http4s re-platform (2026) + +The entire OBP-API HTTP surface — every API version from v1.2.1 to v7.0.0, plus Berlin Group and UK Open Banking — now runs on [http4s](https://http4s.org/) and Cats IO. Lift Web and Jetty have been removed entirely. + +Key points: + +* Done **in place**, endpoint by endpoint, with zero API version bumps and zero signature changes — API versioning at OBP is tech-agnostic, so a framework migration is invisible to API consumers. (This is also why this milestone appears here and not in the API catalogs: it changed no API contract.) +* Purely functional Scala (Cats Effect), non-blocking I/O. +* Fewer dependencies; unblocks the Scala 2.13 / 3.x upgrade (see [roadmap.md](roadmap.md), Theme 3 — Lift Mapper persistence is the remaining Lift dependency). * New / Enhanced support for Berlin Group, STET, UK Open Banking, Polish and Australia CDR APIs e.g. diff --git a/roadmap.md b/roadmap.md index 4b7e6fdaef..6dd82115f8 100644 --- a/roadmap.md +++ b/roadmap.md @@ -1,44 +1,83 @@ -# Welcome to the OBP roadmap! +# OBP Roadmap -This document contains +_Last reviewed: 10 July 2026_ -* Current developments -* A link to [completed_developments](completed_developments.md) -* The future (as much as anyone can know!) +This is the living roadmap for OBP-API and closely related OBP projects. It is organised by **theme** and **horizon** (Active / Next / Later), not by feature-and-date, because OBP's priorities are steered — deliberately — by three forces: -Our roadmap is agile and likely to be modified / re-prioritised based on demand from banks and developers. It should be seen as an indication of direction rather than something set in stone. +1. **Client demand** — banks and organisations using OBP in production steer what gets built next. +2. **Regulation** — external regulatory deadlines (PSD2/PSD3/PSR, FIDA, CDR, UK Open Banking) are the only items on this roadmap with dates, because those dates are set by regulators, not by us. +3. **Technology shifts** — when something like MCP/agentic AI arrives, we re-prioritise fast. We consider this a feature of the project, not a failure of planning. -This document mainly concerns OBP API but may reference other OBP projects. +**Horizons:** -If you have a particular requirement or would like to comment or help us specify, please [get in touch](http://www.openbankproject.com/contact) or make a pull request to this document. +- **Active** — in active development. +- **Next** — committed direction, expected to start when current work lands or a client engagement triggers it. +- **Later** — on our radar; will be pulled forward by client demand or regulation. +For what has already shipped, see the various API catalogs, glossary and README — those are the authoritative record of API functionality. Architectural milestones that change no API signature (and so don't appear in the catalogs) are logged in [completed_developments.md](completed_developments.md). -## Current developments +If you have a requirement, want to comment, or want to pull an item forward, [get in touch](https://www.openbankproject.com/contact). +--- +## Regulatory & standards commitments (the only dated track) -## Completed developments +| Item | External date | Our status | +| ---------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| PSD3 / PSR | Final texts agreed Apr 2026; application expected ~2027–28 | Tracking; gap analysis against final RTS when published | +| FIDA (EU open finance) | Still in trilogue; operational ~2029 (est.) | Watching; OBP dynamic entities/endpoints already support FIDA-style non-payment datasets | -See [completed_developments.md](completed_developments.md) +--- -## Roadmap +## Theme 1: Standards & regulatory coverage -### Version 4.0.0 +_The standard is an adapter, not the architecture — one core serves many standards._ -* Add Accounts query API (by Product Code etc.) -* Model Offers -* Model Direct Debits -* Model Future Payments with Transaction Requests -* Create scripts to populate Products for - * Fixed Term Deposits (size, term, interest rate, constraints) - * Savings accounts (interest rate, constraints) -* Customer Portfolio summary endpoint. -* Clarify Account Customer Owners. +- **Active:** UK Open Banking v4.x refresh (AIS first, then PIS). +- **Next:** PSD3/PSR alignment work as final technical standards emerge. +- **Later:** FIDA data-sharing schemes; further Berlin Group version updates as published. -* Auto feed of Firehose Accounts/Transactions/Customers into Elastic Search +## Theme 2: AI & agentic access +- **Active:** OBP MCP server — agents (Claude, others) driving OBP APIs: account information, payments with consent, service discovery. Consent scoping, audit trail and human-in-the-loop controls for agent-initiated actions. +- **Next:** Hardening the agent consent/security model; agent-friendly API metadata (schemas, glossary, resource docs as agent context). +- **Later:** Agent-to-agent flows; agentic commerce standards as they stabilise. -### SDK Documentation Upgrade +## Theme 3: Core platform & architecture -* Update SDK docs so API root links are clearly place holders -* Create second generation SDKs \ No newline at end of file +_The Lift Web → http4s re-platform is **complete**: the entire API surface, every version, now runs on http4s and Cats IO with no Lift Web and no Jetty — done in place, with zero API signature changes. See [completed_developments.md](completed_developments.md)._ + +- **Active:** **OBP-Dispatch** — lightweight router directing requests between OBP-API and OBP-Trading instances, using Resource Docs for discovery. +- **Next:** Replace Lift Mapper — the last remaining Lift dependency — with a modern persistence layer; OBP Scala Library adoption. +- **Later:** Scala 2.13 / 3.x upgrade (unblocked by the Lift Web removal; gated on the persistence work). + +## Theme 4: Bank-side integration + +- **Active:** Connector/adapter maintenance for production client banks (REST, Kafka, MQ, stored-procedure connectors). +- **Next:** Integration patterns for coexistence with incumbent API gateways (e.g. MuleSoft, Apigee) — documentation and reference architecture. +- **Later:** Additional core-banking adapter templates driven by client engagements. + +## Theme 5: Developer experience + +- **Active:** API Explorer, developer portal, sandbox data tooling. +- **Next:** Second-generation SDKs; improved onboarding for TPP developers. +- **Later:** — + +## Theme 6: Security, operations & certification + +- **Active:** Rate limiting enhancements; ongoing security hardening. +- **Next:** FAPI conformance review; certification targets per standard (e.g. OBIE functional conformance) as client demand requires. +- **Later:** — + +--- + +## How this roadmap is maintained + +- Reviewed and re-stamped (see date at top) at least **quarterly**, and whenever a major re-prioritisation happens. +- Items move between horizons freely; that is the system working, not the system failing. +- When an organisation asks us for "the roadmap", we send a dated snapshot of this file — we do not maintain separate per-organisation roadmap documents. +- Shipped work leaves this file. API-visible work needs no separate record — it appears in the Resource Docs, API Explorer and glossary automatically. Architectural milestones that change no API signature (re-platforms, dependency removals, performance work) move to [completed_developments.md](completed_developments.md), which is a curated log of exactly that category — not a complete history. + +## History + +This document supersedes the previous roadmap.md content (last updated 2019) and the Roadmap page on the GitHub wiki (2018), which now points here. From f465fd83917966489cc26a413eba86b8cfa3aebb Mon Sep 17 00:00:00 2001 From: simonredfern Date: Tue, 14 Jul 2026 18:54:29 +0200 Subject: [PATCH 2/3] Removing Hydra in favour of OBP-OIDC for UK standard. --- dependency-check-suppressions.xml | 14 -- docs/IdP/OAUTH2_IDENTITY_PROVIDERS.md | 20 ++- obp-api/pom.xml | 9 -- .../docs/introductory_system_documentation.md | 141 ++---------------- .../resources/props/sample.props.template | 24 +-- .../main/scala/bootstrap/liftweb/Boot.scala | 31 ---- obp-api/src/main/scala/code/api/OAuth2.scala | 135 ++--------------- .../OpenAPI31JSONFactory.scala | 2 +- .../main/scala/code/api/util/APIUtil.scala | 5 - .../scala/code/api/util/ConsentUtil.scala | 30 ++-- .../scala/code/api/util/ErrorMessages.scala | 3 + .../main/scala/code/api/util/Glossary.scala | 2 +- obp-api/src/main/scala/code/model/OAuth.scala | 25 ---- .../code/model/dataAccess/AuthUser.scala | 3 - .../src/main/scala/code/util/HydraUtil.scala | 137 ----------------- 15 files changed, 67 insertions(+), 514 deletions(-) delete mode 100644 obp-api/src/main/scala/code/util/HydraUtil.scala diff --git a/dependency-check-suppressions.xml b/dependency-check-suppressions.xml index 3dc5bae678..49ad8ab37b 100644 --- a/dependency-check-suppressions.xml +++ b/dependency-check-suppressions.xml @@ -1,20 +1,6 @@ - - - ^pkg:maven/sh\.ory\.hydra/hydra-client@.*$ - CVE-2026-33504 - - java-scriptengine 2.0.0 - - - sh.ory.hydra - hydra-client - 1.7.0 - com.fasterxml.jackson.core jackson-databind 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 d20d8931a6..1649035b44 100644 --- a/obp-api/src/main/resources/docs/introductory_system_documentation.md +++ b/obp-api/src/main/resources/docs/introductory_system_documentation.md @@ -22,12 +22,11 @@ For more detailed information or the sources of truths, please refer to the indi - 3.4 [Opey II (AI Agent)](#34-opey-ii-ai-agent) - 3.5 [OBP-OIDC (Development Provider)](#35-obp-oidc-development-provider) - 3.6 [Keycloak Integration (Production Provider)](#36-keycloak-integration-production-provider) - - 3.7 [Ory Hydra (Production Provider)](#37-ory-hydra-production-provider) - - 3.8 [OBP-Hola](#38-obp-hola) - - 3.9 [OBP-SEPA-Adapter](#39-obp-sepa-adapter) - - 3.10 [OBP-Rabbit-Cats-Adapter](#310-obp-rabbit-cats-adapter) - - 3.11 [Connectors](#311-connectors) - - 3.12 [Adapters](#312-adapters) + - 3.7 [OBP-Hola](#37-obp-hola) + - 3.8 [OBP-SEPA-Adapter](#38-obp-sepa-adapter) + - 3.9 [OBP-Rabbit-Cats-Adapter](#39-obp-rabbit-cats-adapter) + - 3.10 [Connectors](#310-connectors) + - 3.11 [Adapters](#311-adapters) - 3.13 [Message Docs](#313-message-docs) - 3.14 [OBP-MCP (Model Context Protocol Server)](#314-obp-mcp-model-context-protocol-server) 4. [Standards Compliance](#standards-compliance) @@ -395,7 +394,7 @@ The Open Bank Project (OBP) is an open-source RESTful API platform for banks tha **OIDC Providers:** -- Production: Keycloak, Hydra, Google, Yahoo, Auth0, Azure AD +- Production: Keycloak, Google, Yahoo, Auth0, Azure AD - Development/Testing: OBP-OIDC --- @@ -739,120 +738,7 @@ docker pull openbankproject/obp-keycloak:main-themed --- -### 3.7 Ory Hydra (Production Provider) - -**Purpose:** Cloud-native OAuth2 and OpenID Connect server for production deployments - -**Overview:** - -Ory Hydra is a hardened, open-source OAuth 2.0 and OpenID Connect server optimized for low-latency, high-throughput, and low resource consumption. It integrates with OBP-API to provide enterprise-grade authentication and authorization. - -**Key Features:** - -- **OAuth2 & OIDC Compliance:** Full implementation of OAuth 2.0 and OpenID Connect specifications -- **Cloud Native:** Designed for containerized deployments (Docker, Kubernetes) -- **Performance:** Low latency and high throughput -- **Separation of Concerns:** Hydra handles OAuth/OIDC flow; identity management delegated to custom Identity Provider -- **Security Hardened:** Regular security audits and compliance certifications -- **Storage Backend:** PostgreSQL, MySQL, CockroachDB support - -**Architecture:** - -``` -Client → Hydra (OAuth2 Server) → OBP Hydra Identity Provider → OBP-API - ↓ - Database (PostgreSQL) -``` - -**Components:** - -- **Ory Hydra:** OAuth2/OIDC server -- **OBP Hydra Identity Provider:** Custom login/consent UI and user management -- **OBP-API:** Banking API with Hydra integration - -**OBP-API Configuration:** - -```properties -# Enable Hydra login -login_with_hydra=true - -# Hydra server URLs -hydra_public_url=http://127.0.0.1:4444 -hydra_admin_url=http://127.0.0.1:4445 - -# Consent scopes -hydra_consents=ReadAccountsBasic,ReadAccountsDetail,ReadBalances,ReadTransactionsBasic,ReadTransactionsDebits,ReadTransactionsDetail - -# JWKS validation -oauth2.jwk_set.url=http://127.0.0.1:4444/.well-known/jwks.json - -# Mirror consumers to Hydra clients -mirror_consumer_in_hydra=true -``` - -**Hydra Identity Provider Configuration:** - -```properties -# Server port -server.port=8086 - -# OBP-API URL -obp.base_url=http://localhost:8080 -endpoint.path.prefix=${obp.base_url}/obp/v4.0.0 - -# Hydra admin URL -oauth2.admin_url=http://127.0.0.1:4445 - -# Service account credentials -identity_provider.user.username=serviceuser -identity_provider.user.password=password -consumer_key=your-consumer-key - -# mTLS configuration (optional) -mtls.keyStore.path=file:///path/to/keystore.jks -mtls.keyStore.password=keystore-password -mtls.trustStore.path=file:///path/to/truststore.jks -mtls.trustStore.password=truststore-password -``` - -**Docker Deployment:** - -```bash -# Start Hydra with docker-compose -docker-compose -f quickstart.yml \ - -f quickstart-postgres.yml \ - up --build - -# Verify Hydra is running -curl http://127.0.0.1:4444/.well-known/openid-configuration -``` - -**Hydra quickstart.yml environment:** - -```yaml -environment: - - URLS_CONSENT=http://localhost:8086/consent - - URLS_LOGIN=http://localhost:8086/login - - URLS_LOGOUT=http://localhost:8086/logout -``` - -**Use Cases:** - -- High-performance OAuth2/OIDC deployments -- Microservices architectures requiring centralized authentication -- Multi-tenant banking platforms -- Open Banking TPP integrations -- Cloud-native banking solutions - -**Repositories:** - -- Ory Hydra: https://github.com/ory/hydra -- OBP Hydra Identity Provider: https://github.com/OpenBankProject/OBP-Hydra-Identity-Provider -- Demo OAuth2 Client: https://github.com/OpenBankProject/OBP-Hydra-OAuth2 - ---- - -### 3.8 OBP-Hola +### 3.7 OBP-Hola **Purpose:** Reference implementation for OAuth2 authentication and consent flow testing @@ -878,8 +764,7 @@ OBP-Hola is a Java/Spring Boot application that demonstrates and tests OBP authe **Dependencies:** - **OBP-API:** Core banking API -- **Ory Hydra:** OAuth2 server -- **OBP Hydra Identity Provider:** Identity management +- **An OIDC provider:** OBP-OIDC (development) or Keycloak (production) **Use Cases:** @@ -934,7 +819,7 @@ docker run -p 48123:48123 \ --- -### 3.9 OBP-SEPA-Adapter +### 3.8 OBP-SEPA-Adapter **Purpose:** Reference implementation for SEPA payment processing with OBP-API @@ -1092,7 +977,7 @@ sbt "runMain sepa.scheduler.ProcessIncomingFilesActorSystem" --- -### 3.10 OBP-Rabbit-Cats-Adapter +### 3.9 OBP-Rabbit-Cats-Adapter **Purpose:** OBP-Rabbit-Cats-Adapter is a functional, type-safe adapter that bridges OBP-API with Core Banking Systems (CBS) using RabbitMQ messaging and/or gRPC. It is built on a modern Scala stack using Cats Effect and http4s. @@ -1188,7 +1073,7 @@ When gRPC is enabled, the adapter starts a gRPC server alongside the RabbitMQ co --- -### 3.11 Connectors +### 3.10 Connectors **Purpose:** Connectors provide the integration layer between OBP-API and backend banking systems or data sources. @@ -1257,7 +1142,7 @@ When gRPC is enabled, the adapter starts a gRPC server alongside the RabbitMQ co --- -### 3.12 Adapters +### 3.11 Adapters **Purpose:** Adapters are backend services that receive messages from OBP-API connectors and respond according to Message Doc definitions. @@ -2677,7 +2562,7 @@ Authorization: Bearer ACCESS_TOKEN **Providers:** -- **Production:** Keycloak, Hydra, Google, Yahoo, Auth0, Azure AD +- **Production:** Keycloak, Google, Yahoo, Auth0, Azure AD - **Development:** OBP-OIDC **Configuration Example (Keycloak):** diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index d5616d5621..fb80fe8198 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -1330,27 +1330,15 @@ outboundAdapterCallContext.generalContext #connector.name.export.as.endpoints=stored_procedure_vDec2019 # ------------------------------------------------------------------------------------------ -# ------------------------------ Hydra oauth2 props ------------------------------ -## if integrate_with_hydra set to true, all other props must not be empty -#integrate_with_hydra=true -#hydra_public_url=http://127.0.0.1:4444 -#hydra_admin_url=http://127.0.0.1:4445 -#hydra_consents=ReadAccountsBasic,ReadAccountsDetail,ReadBalances,ReadTransactionsBasic,ReadTransactionsDebits,ReadTransactionsDetail -## check the oauth2.jwk_set.url props, it must contains jwks.json that locate in ${hydra_public_url}/.well-known/jwks.json -##oauth2.jwk_set.url=http://localhost:4444/.well-known/jwks.json,https://www.googleapis.com/oauth2/v3/certs -## whether create hydra client when create consumer, default is false -#mirror_consumer_in_hydra=true -# There are 2 ways of authenticating OAuth 2.0 Clients at the /oauth2/token we support: private_key_jwt and client_secret_post -# hydra_token_endpoint_auth_method=private_key_jwt -# hydra_supported_token_endpoint_auth_methods=client_secret_basic,client_secret_post,private_key_jwt -## ORY Hydra login url is "obp-api-hostname/user_mgt/login" implies "true" in order to avoid creation of a new user during OIDC flow -# hydra_uses_obp_user_credentials=true -# ------------------------------ Hydra oauth2 props end ------------------------------ - # ------------------------------ OBP-OIDC oauth2 props ------------------------------ ## OBP-OIDC Provider Configuration -## Choose which OIDC provider to use: 'keycloak' or 'obp-oidc' +## OIDC provider(s) to advertise via the /well-known endpoint: 'keycloak', 'obp-oidc', +## or a comma-separated list to run several in parallel (tokens are dispatched per +## request by their iss claim, so e.g. OBP-OIDC and Keycloak can serve side by side). #oauth2.oidc_provider=obp-oidc +## Consent-bound flows (e.g. UK Open Banking AIS) additionally require the provider to +## include a consent_id claim in the access tokens it issues during the consent +## authorisation flow — OBP-API resolves and validates the consent from its own database. ## OBP-OIDC OAuth2 Provider Settings #oauth2.obp_oidc.host=http://localhost:9000 diff --git a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala index 52f1c14c78..54bcc7ae48 100644 --- a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala @@ -141,7 +141,6 @@ import code.customerlinks.CustomerLink import code.userlocks.UserLocks import code.users._ import code.util.Helper.MdcLoggable -import code.util.HydraUtil import code.validation.JsonSchemaValidation import code.views.Views import code.views.system.{AccountAccess, ViewDefinition, ViewPermission} @@ -160,7 +159,6 @@ import net.liftweb.util._ import org.apache.commons.io.FileUtils import java.io.{File, FileInputStream} -import java.util.stream.Collectors import java.util.{Locale, TimeZone} import scala.util.control.NonFatal @@ -582,35 +580,6 @@ class Boot extends MdcLoggable { // ConnectorEndpoints (connector.name.export.as.endpoints) registered via Lift statelessDispatch // which is no longer reachable (Lift bridge removed in Phase B). Disabled until migrated to http4s. - if(HydraUtil.integrateWithHydra && HydraUtil.mirrorConsumerInHydra) { - createHydraClients() - } - - } - - // create Hydra client if exists active consumer but missing Hydra client - def createHydraClients() = { - try { - import scala.concurrent.ExecutionContext.Implicits.global - // exists hydra clients id - val oAuth2ClientIds = HydraUtil.hydraAdmin.listOAuth2Clients(Long.MaxValue, 0L).stream() - .map[String](_.getClientId) - .collect(Collectors.toSet()) - - Consumers.consumers.vend.getConsumersFuture(Nil, None).foreach{ consumers => - consumers.filter(consumer => consumer.isActive.get && !oAuth2ClientIds.contains(consumer.key.get)) - .foreach(HydraUtil.createHydraClient(_)) - } - } catch { - case e: Exception => - if(HydraUtil.integrateWithHydra) { - logger.error("------------------------------ Mirror consumer in hydra issue ------------------------------") - e.printStackTrace() - } else { - logger.warn("------------------------------ Mirror consumer in hydra issue ------------------------------") - logger.warn(e) - } - } } def schemifyAll() = { diff --git a/obp-api/src/main/scala/code/api/OAuth2.scala b/obp-api/src/main/scala/code/api/OAuth2.scala index 7eda828616..869501ff8c 100644 --- a/obp-api/src/main/scala/code/api/OAuth2.scala +++ b/obp-api/src/main/scala/code/api/OAuth2.scala @@ -35,8 +35,6 @@ import code.model.{AppType, Consumer} import code.scope.Scope import code.users.Users import code.util.Helper.MdcLoggable -import code.util.HydraUtil -import code.util.HydraUtil._ import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet import com.openbankproject.commons.ExecutionContext.Implicits.global @@ -45,7 +43,6 @@ import net.liftweb.common.Box.tryo import net.liftweb.common._ import net.liftweb.util.Helpers import org.apache.commons.lang3.StringUtils -import sh.ory.hydra.model.OAuth2TokenIntrospection import java.net.URI import scala.concurrent.Future @@ -56,8 +53,8 @@ import scala.collection.JavaConverters._ * so they could authenticate their users. */ -// OAuth2Login is a Bearer-token validator (Google / Yahoo / Azure / Keycloak / OBPOIDC / -// Hydra) consumed by `APIUtil.getUserFuture` and `OBPRestHelper.OAuth2.getUser`. It has no +// OAuth2Login is a Bearer-token validator (Google / Yahoo / Azure / Keycloak / OBPOIDC) +// consumed by `APIUtil.getUserFuture` and `OBPRestHelper.OAuth2.getUser`. It has no // HTTP routes of its own — the legacy `extends RestHelper` mixin was vestigial. object OAuth2Login extends MdcLoggable { @@ -86,8 +83,6 @@ object OAuth2Login extends MdcLoggable { Keycloak.applyRules(value, cc) } else if (UnknownProvider.isIssuer(value)) { UnknownProvider.applyRules(value, cc) - } else if (HydraUtil.integrateWithHydra) { - Hydra.applyRules(value, cc) } else { (Failure(Oauth2IsNotRecognized), Some(cc)) } @@ -115,8 +110,6 @@ object OAuth2Login extends MdcLoggable { Keycloak.applyRulesFuture(value, cc) } else if (UnknownProvider.isIssuer(value)) { UnknownProvider.applyRulesFuture(value, cc) - } else if (HydraUtil.integrateWithHydra) { - Hydra.applyRulesFuture(value, cc) } else { Future(Failure(Oauth2IsNotRecognized), Some(cc)) } @@ -138,104 +131,6 @@ object OAuth2Login extends MdcLoggable { } - object Hydra extends OAuth2Util { - override def wellKnownOpenidConfiguration: URI = new URI(hydraPublicUrl) - override def urlOfJwkSets: Box[String] = checkUrlOfJwkSets(identityProvider = hydraPublicUrl) - - override def applyAccessTokenRules(value: String, cc: CallContext): (Box[User], Some[CallContext]) = { - // In case of Hydra issued access tokens are not self-encoded/self-contained like JWT tokens are. - // It implies the access token can be revoked at any time. - val introspectOAuth2Token: OAuth2TokenIntrospection = hydraAdmin.introspectOAuth2Token(value, null) - val hydraClient = hydraAdmin.getOAuth2Client(introspectOAuth2Token.getClientId()) - var consumer: Box[Consumer] = consumers.vend.getConsumerByConsumerKey(introspectOAuth2Token.getClientId) - logger.debug("introspectOAuth2Token.getIss: " + introspectOAuth2Token.getIss) - logger.debug("introspectOAuth2Token.getActive: " + introspectOAuth2Token.getActive) - logger.debug("introspectOAuth2Token.getClientId: " + introspectOAuth2Token.getClientId) - logger.debug("introspectOAuth2Token.getAud: " + introspectOAuth2Token.getAud) - logger.debug("introspectOAuth2Token.getUsername: " + introspectOAuth2Token.getUsername) - logger.debug("introspectOAuth2Token.getExp: " + introspectOAuth2Token.getExp) - logger.debug("introspectOAuth2Token.getNbf: " + introspectOAuth2Token.getNbf) - // The access token can be disabled at any time due to fact it is NOT self-encoded/self-contained. - if (!introspectOAuth2Token.getActive) { - return (Failure(Oauth2IJwtCannotBeVerified), Some(cc.copy(consumer = Failure(Oauth2IJwtCannotBeVerified)))) - } - if (!hydraSupportedTokenEndpointAuthMethods.contains(hydraClient.getTokenEndpointAuthMethod())) { - logger.debug("hydraClient.getTokenEndpointAuthMethod(): " + hydraClient.getTokenEndpointAuthMethod().toLowerCase()) - val errorMessage = Oauth2TokenEndpointAuthMethodForbidden + hydraClient.getTokenEndpointAuthMethod() - return (Failure(errorMessage), Some(cc.copy(consumer = Failure(errorMessage)))) - } - - // check access token binding with client certificate - { - if(consumer.isEmpty) { - return (Failure(Oauth2TokenHaveNoConsumer), Some(cc.copy(consumer = Failure(Oauth2TokenHaveNoConsumer)))) - } - val clientCert: Option[String] = APIUtil.`getPSD2-CERT`(cc.requestHeaders) - clientCert.filter(StringUtils.isNotBlank).foreach {cert => - val foundConsumer = consumer.orNull - val certInConsumer = foundConsumer.clientCertificate.get - if(StringUtils.isBlank(certInConsumer)) { - // In case that the certificate of a consumer is not populated in a database - // we use the value at PSD2-CERT header in order to populate it for the first time. - // Please note that every next call MUST match that value. - foundConsumer.clientCertificate.set(cert) - consumer = Full(foundConsumer.saveMe()) - val clientId = foundConsumer.key.get - // update hydra client client_certificate - val oAuth2Client = hydraAdmin.getOAuth2Client(clientId) - val clientMeta = oAuth2Client.getMetadata.asInstanceOf[java.util.Map[String, AnyRef]] - if(clientMeta == null) { - oAuth2Client.setMetadata(Map("client_certificate" -> cert).asJava) - } else { - clientMeta.put("client_certificate", cert) - } - // hydra update client endpoint have bug, So here delete and create to do update - hydraAdmin.deleteOAuth2Client(clientId) - hydraAdmin.createOAuth2Client(oAuth2Client) - } else if(!CertificateUtil.comparePemX509Certificates(certInConsumer, cert)) { - // Cannot mat.ch the value from PSD2-CERT header and the database value Consumer.clientCertificate - logger.debug(s"Cert in Consumer with the name ***${foundConsumer.name}*** : " + certInConsumer) - logger.debug("Cert in Request: " + cert) - logger.debug(s"Token: $value") - logger.debug(s"Client ID: ${introspectOAuth2Token.getClientId}") - return (Failure(Oauth2TokenMatchCertificateFail), Some(cc.copy(consumer = Failure(Oauth2TokenMatchCertificateFail)))) - } else { - // Certificate is matched. Just make some debug logging. - logger.debug("The token is linked with a proper client certificate.") - logger.debug(s"Token: $value") - logger.debug(s"Client Key: ${introspectOAuth2Token.getClientId}") - } - } - } - - // In case a user is created via OpenID Connect flow implies provider = hydraPublicUrl - // In case a user is created via GUI of OBP-API implies provider = Constant.localIdentityProvider - val user = Users.users.vend.getUserByProviderAndUsername(introspectOAuth2Token.getIss, introspectOAuth2Token.getSub).or( - Users.users.vend.getUserByProviderAndUsername(Constant.localIdentityProvider, introspectOAuth2Token.getSub) - ) - user match { - case Full(u) => - LoginAttempt.userIsLocked(u.provider, u.name) match { - case true => (Failure(UsernameHasBeenLocked), Some(cc.copy(consumer = consumer))) - case false => (Full(u), Some(cc.copy(consumer = consumer))) - } - case _ => (user, Some(cc.copy(consumer = consumer))) - } - } - - def applyRules(token: String, cc: CallContext): (Box[User], Some[CallContext]) = { - isIssuer(jwtToken=token, identityProvider = hydraPublicUrl) match { - case true => super.applyIdTokenRules(token, cc) - case false => applyAccessTokenRules(token, cc) - } - } - - def applyRulesFuture(value: String, cc: CallContext): Future[(Box[User], Some[CallContext])] = Future { - applyRules(value, cc) - } - - } - trait OAuth2Util { def wellKnownOpenidConfiguration: URI @@ -564,23 +459,15 @@ object OAuth2Login extends MdcLoggable { case Some(provider) => logger.debug(s"resolveProvider says: using provider from token claim: $provider") provider - case None => - // Fallback to existing logic if provider claim is not present or blank - HydraUtil.integrateWithHydra && isIssuer(jwtToken = jwtToken, identityProvider = hydraPublicUrl) match { - case true if HydraUtil.hydraUsesObpUserCredentials => // Case that source of the truth of Hydra user management is the OBP-API mapper DB - logger.debug(s"resolveProvider says: we are in Hydra, use Constant.localIdentityProvider ${Constant.localIdentityProvider}") - // In case that ORY Hydra login url is "hostname/user_mgt/login" we MUST override hydraPublicUrl as provider - // in order to avoid creation of a new user - Constant.localIdentityProvider - // if its OBPOIDC issuer - case false if OBPOIDC.isIssuer(jwtToken) => - logger.debug(s"resolveProvider says: we are in OBP-OIDC, use Constant.localIdentityProvider ${Constant.localIdentityProvider}") - Constant.localIdentityProvider - case _ => // All other cases implies a new user creation - logger.debug("resolveProvider says: Other cases ") - // TODO raise exception in case of else case - JwtUtil.getIssuer(jwtToken).getOrElse("") - } + case None if OBPOIDC.isIssuer(jwtToken) => + // OBP-OIDC authenticates against the OBP-API user store, so its users ARE local users — + // use the local provider to avoid creating a duplicate user during the OIDC flow. + logger.debug(s"resolveProvider says: we are in OBP-OIDC, use Constant.localIdentityProvider ${Constant.localIdentityProvider}") + Constant.localIdentityProvider + case None => // All other cases implies a new user creation + logger.debug("resolveProvider says: Other cases ") + // TODO raise exception in case of else case + JwtUtil.getIssuer(jwtToken).getOrElse("") } } diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/OpenAPI31JSONFactory.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/OpenAPI31JSONFactory.scala index dfbe35785f..604a466236 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/OpenAPI31JSONFactory.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/OpenAPI31JSONFactory.scala @@ -514,7 +514,7 @@ object OpenAPI31JSONFactory extends MdcLoggable { in = Some("header") ), // OBP API consumes Bearer tokens issued by external IdPs (Google, Yahoo, - // Azure, Keycloak, Hydra) — it does not issue its own OAuth2 tokens. The + // Azure, Keycloak, OBP-OIDC) — it does not issue its own OAuth2 tokens. The // accurate OpenAPI representation is `type: http, scheme: bearer`, not an // `oauth2` flow with token-issuance URLs. "OAuth2" -> SecuritySchemeJson( 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 f92e74ffa1..cd283759a1 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -3584,11 +3584,6 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ */ def getServerUrl: String = getPropsValue("documented_server_url").openOr(MissingPropsValueAtThisInstance + "documented_server_url") - /** - * This value is used to construct some urls in Glossary - */ - def getHydraPublicServerUrl: String = getPropsValue("hydra_public_url").openOr(MissingPropsValueAtThisInstance + "hydra_public_url") - // All OBP REST end points start with /obp def getObpApiRoot: String = s"$getServerUrl/obp" diff --git a/obp-api/src/main/scala/code/api/util/ConsentUtil.scala b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala index 6628bbf141..ef4c4ab9a6 100644 --- a/obp-api/src/main/scala/code/api/util/ConsentUtil.scala +++ b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala @@ -23,7 +23,6 @@ import code.model.dataAccess.BankAccountRouting import code.scheduler.ConsentScheduler.currentDate import code.users.Users import code.util.Helper.MdcLoggable -import code.util.HydraUtil import code.views.Views import com.nimbusds.jwt.JWTClaimsSet import com.openbankproject.commons.ExecutionContext.Implicits.global @@ -35,7 +34,6 @@ import org.json4s.{Extraction, MappingException} import com.openbankproject.commons.util.JsonAliases.{compactRender, parse} import net.liftweb.mapper.By import net.liftweb.util.Props -import sh.ory.hydra.model.OAuth2TokenIntrospection import java.text.SimpleDateFormat import java.util.Date @@ -1109,19 +1107,25 @@ object Consent extends MdcLoggable { } + /** + * Validates the UK Open Banking AIS consent bound to the presented OAuth2 access token. + * + * Provider contract (OBP-OIDC, Keycloak, or any other OIDC server): the Bearer access token + * is a JWT carrying a `consent_id` claim, planted by the identity provider during the consent + * authorisation flow. Signature, issuer and expiry of the token were already verified by the + * authentication layer (OAuth2Login issuer dispatch) before any endpoint reaches this check, + * so only the claim is extracted here. The OBP database remains authoritative for consent + * state: the status/consumer checks below run on every request, so revoking a consent takes + * effect immediately even though an already-issued JWT cannot itself be revoked. + */ def checkUKConsent(user: User, calContext: Option[CallContext]): Box[Boolean] = { - val accessToken = calContext.flatMap(_.authReqHeaderField) - .map(_.replaceFirst("Bearer\\s+", "")) - .getOrElse(throw new RuntimeException("Not found http request header 'Authorization', it is mandatory.")) - val introspectOAuth2Token: OAuth2TokenIntrospection = HydraUtil.hydraAdmin.introspectOAuth2Token(accessToken, null) - if(!introspectOAuth2Token.getActive) { - return Failure(ErrorMessages.ConsentExpiredIssue) - } + val accessToken = calContext.flatMap(_.authReqHeaderField) + .map(_.replaceFirst("Bearer\\s+", "")) + .getOrElse(throw new RuntimeException("Not found http request header 'Authorization', it is mandatory.")) - val boxedConsent: Box[MappedConsent] = { - val accessExt = introspectOAuth2Token.getExt.asInstanceOf[java.util.Map[String, String]] - val consentId = accessExt.get("consent_id") - Consents.consentProvider.vend.getConsentByConsentId(consentId) + val boxedConsent: Box[MappedConsent] = JwtUtil.getOptionalClaim("consent_id", accessToken) match { + case Some(consentId) => Consents.consentProvider.vend.getConsentByConsentId(consentId) + case None => return Failure(ErrorMessages.ConsentIdClaimMissing) } boxedConsent match { diff --git a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala index c55f7a9f2d..cd330ed417 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -768,6 +768,7 @@ object ErrorMessages { val ConsentHeaderValueInvalid = "OBP-35032: The Consent's Request Header value is not formatted as UUID or JWT." val RolesForbiddenInConsent = s"OBP-35033: Consents cannot contain the following Roles: ${canCreateEntitlementAtOneBank} and ${canCreateEntitlementAtAnyBank}." val UserAuthContextUpdateRequestAllowedScaMethods = "OBP-35034: Unsupported as SCA method. " + val ConsentIdClaimMissing = "OBP-35035: The access token is not bound to a Consent. The identity provider must include a consent_id claim in access tokens issued via the consent authorisation flow. " //Authorisations val AuthorisationNotFound = "OBP-36001: Authorisation not found. Please specify valid values for PAYMENT_ID and AUTHORISATION_ID. " @@ -1065,6 +1066,8 @@ object ErrorMessages { EntitlementCannotBeDeleted -> 500, ConsentStatusIssue -> 401, ConsentDisabled -> 401, + // 403 not 401: the token authenticated fine, it just isn't bound to a consent (UK OB: insufficient authorisation) + ConsentIdClaimMissing -> 403, InternalServerError -> 500, ) 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 71423cce2e..215c14bbba 100644 --- a/obp-api/src/main/scala/code/api/util/Glossary.scala +++ b/obp-api/src/main/scala/code/api/util/Glossary.scala @@ -1544,7 +1544,7 @@ object Glossary extends MdcLoggable { | |Direct Login is built into OBP and used for testing purposes / local installations. | -|OAuth2 / Open ID Connect (OIDC) is the recommended method for production use, and depends on the configuration of Identity Provider solutions such as Keycloak or Hydra or external services such as Google or Yahoo. +|OAuth2 / Open ID Connect (OIDC) is the recommended method for production use, and depends on the configuration of Identity Provider solutions such as Keycloak or OBP-OIDC or external services such as Google or Yahoo. | |Open Bank Project can support multiple identity providers per OBP instance. For example, for a single OBP installation, some Users could authenticate against Google and some could authenticate against a local identity provider. |In the cases where multiple identity providers are configured, OBP differentiates between Users by not only their Username but also by their "Identity Provider". i.e. J.Brown logged in via Google is distinct from J.Brown who logged in via a local OBP instance. diff --git a/obp-api/src/main/scala/code/model/OAuth.scala b/obp-api/src/main/scala/code/model/OAuth.scala index 964e1ff970..ed88cb1cf6 100644 --- a/obp-api/src/main/scala/code/model/OAuth.scala +++ b/obp-api/src/main/scala/code/model/OAuth.scala @@ -35,7 +35,6 @@ import code.nonce.NoncesProvider import code.token.TokensProvider import code.users.Users import code.util.Helper.MdcLoggable -import code.util.HydraUtil._ import com.github.dwickern.macros.NameOf import com.openbankproject.commons.ExecutionContext.Implicits.global import net.liftweb.common._ @@ -223,7 +222,6 @@ object MappedConsumersProvider extends ConsumersProvider with MdcLoggable { } def deleteConsumer(consumer: Consumer): Boolean = { - if(integrateWithHydra) hydraAdmin.deleteOAuth2Client(consumer.key.get) Consumer.delete_!(consumer) } @@ -294,28 +292,6 @@ object MappedConsumersProvider extends ConsumersProvider with MdcLoggable { } val updatedConsumer = c.saveMe() - // In case we use Hydra ORY as Identity Provider we update corresponding client at Hydra side a well - if(integrateWithHydra && isActive.isDefined) { - val clientId = c.key.get - val existsOAuth2Client = Box.tryo(hydraAdmin.getOAuth2Client(clientId)) - .filter(null.!=) - // TODO Involve Hydra ORY version with working update mechanism - if (isActive == Some(false) && existsOAuth2Client.isDefined) { - existsOAuth2Client - .map { oAuth2Client => - oAuth2Client.setClientSecretExpiresAt(System.currentTimeMillis()) - hydraAdmin.updateOAuth2Client(clientId, oAuth2Client) - } - } - if(isActive == Some(true) && existsOAuth2Client.isDefined) { - existsOAuth2Client - .map { oAuth2Client => - oAuth2Client.setClientSecretExpiresAt(0L) - hydraAdmin.updateOAuth2Client(clientId, oAuth2Client) - } - } - } - updatedConsumer } case _ => consumer @@ -533,7 +509,6 @@ object MappedConsumersProvider extends ConsumersProvider with MdcLoggable { } c.consumerId(actualConsumerId) val createdConsumer = c.saveMe() - if(integrateWithHydra) createHydraClient(createdConsumer) createdConsumer } match { case Full(c) => Full(c) diff --git a/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala b/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala index 35ecf6b8d5..441255c044 100644 --- a/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala +++ b/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala @@ -44,7 +44,6 @@ import code.token.TokensOpenIDConnect import code.users.{UserAgreementProvider, Users} import code.util.Helper import code.util.Helper.{MdcLoggable, ObpS} -import code.util.HydraUtil._ import code.views.Views import code.webuiprops.MappedWebUiPropsProvider.getWebUiPropsValue import com.openbankproject.commons.ExecutionContext.Implicits.global @@ -54,8 +53,6 @@ import net.liftweb.common._ import net.liftweb.mapper._ import net.liftweb.util._ import org.apache.commons.lang3.StringUtils -import sh.ory.hydra.api.AdminApi -import sh.ory.hydra.model.AcceptLoginRequest import java.util.UUID.randomUUID import scala.concurrent.Future diff --git a/obp-api/src/main/scala/code/util/HydraUtil.scala b/obp-api/src/main/scala/code/util/HydraUtil.scala deleted file mode 100644 index a6b1627ed1..0000000000 --- a/obp-api/src/main/scala/code/util/HydraUtil.scala +++ /dev/null @@ -1,137 +0,0 @@ -package code.util - -import java.util.UUID - -import code.api.util.APIUtil -import code.model.Consumer -import code.model.Consumer.redirectURLRegex -import code.util.Helper.MdcLoggable -import com.nimbusds.jose.jwk.gen.{ECKeyGenerator, JWKGenerator, RSAKeyGenerator} -import com.nimbusds.jose.jwk.{AsymmetricJWK, Curve, ECKey, JWK, KeyUse, RSAKey} -import com.nimbusds.jose.{Algorithm, JWSAlgorithm} -import org.apache.commons.lang3.StringUtils -import sh.ory.hydra.api.{AdminApi, PublicApi} -import sh.ory.hydra.model.OAuth2Client -import sh.ory.hydra.{ApiClient, Configuration} - -import scala.collection.immutable.List -import scala.collection.JavaConverters._ - -object HydraUtil extends MdcLoggable{ - - private val INTEGRATE_WITH_HYDRA = "integrate_with_hydra" - - val integrateWithHydra = APIUtil.getPropsAsBoolValue(INTEGRATE_WITH_HYDRA, false) - - val mirrorConsumerInHydra = APIUtil.getPropsAsBoolValue("mirror_consumer_in_hydra", false) - - val hydraUsesObpUserCredentials = APIUtil.getPropsAsBoolValue("hydra_uses_obp_user_credentials", true) - - val clientSecretPost = "client_secret_post" - - val hydraTokenEndpointAuthMethod = - APIUtil.getPropsValue("hydra_token_endpoint_auth_method", "private_key_jwt") - val hydraSupportedTokenEndpointAuthMethods = - APIUtil.getPropsValue("hydra_supported_token_endpoint_auth_methods", "client_secret_basic,client_secret_post,private_key_jwt") - - lazy val hydraPublicUrl: String = APIUtil.getPropsValue("hydra_public_url") - .openOrThrowException(s"The props $INTEGRATE_WITH_HYDRA is $integrateWithHydra, hydra_public_url value should not be blank") - // This method is a regular expression operation that removes a trailing slash (/) from a string if one is present. - .replaceFirst("/$", "") - - lazy val hydraAdminUrl: String = APIUtil.getPropsValue("hydra_admin_url") - .openOrThrowException(s"The props $INTEGRATE_WITH_HYDRA is $integrateWithHydra, hydra_admin_url value should not be blank") - // This method is a regular expression operation that removes a trailing slash (/) from a string if one is present. - .replaceFirst("/$", "") - - lazy val hydraConsents = APIUtil.getPropsValue("hydra_consents") - .openOrThrowException(s"The props $INTEGRATE_WITH_HYDRA is $integrateWithHydra, hydra_client_scope value should not be blank") - .trim.split("""\s*,\s*""").toList - - private lazy val allConsents = hydraConsents.mkString("openid offline email profile ", " ","") - - - val grantTypes = ("authorization_code" :: "client_credentials" :: "refresh_token" :: "implicit" :: Nil).asJava - - lazy val hydraAdmin = { - val hydraAdminUrl = APIUtil.getPropsValue("hydra_admin_url") - .openOrThrowException(s"The props $INTEGRATE_WITH_HYDRA is $integrateWithHydra, hydra_admin_url value should not be blank") - val defaultClient = Configuration.getDefaultApiClient - defaultClient.setBasePath(hydraAdminUrl) - new AdminApi(defaultClient) - } - - lazy val hydraPublic = { - val hydraPublicUrl = APIUtil.getPropsValue("hydra_public_url") - .openOrThrowException(s"The props $INTEGRATE_WITH_HYDRA is $integrateWithHydra, hydra_public_url value should not be blank") - val apiClient = new ApiClient - apiClient.setBasePath(hydraPublicUrl) - new PublicApi(apiClient) - } - - - /** - * create Hydra client, if redirectURL is not valid url, return None - * - * @param consumer - * @return created Hydra client or None - */ - def createHydraClient(consumer: Consumer, fun: OAuth2Client => OAuth2Client = identity): Option[OAuth2Client] = { - logger.info("createHydraClient process is starting") - val redirectUrl = consumer.redirectURL.get - if (StringUtils.isBlank(redirectUrl) || redirectURLRegex.findFirstIn(redirectUrl).isEmpty) { - logger.debug(s"createHydraClient process is aborting because of the redirect url: $redirectUrl at consumer.name: ${consumer.name}") - return None - } - val oAuth2Client = new OAuth2Client() - oAuth2Client.setClientId(consumer.key.get) - oAuth2Client.setClientSecret(consumer.secret.get) - oAuth2Client.setClientName(consumer.name.get) - - oAuth2Client.setScope(allConsents) - - oAuth2Client.setGrantTypes(grantTypes) - oAuth2Client.setResponseTypes(("code" :: "id_token" :: "token" :: "code id_token" :: Nil).asJava) - oAuth2Client.setPostLogoutRedirectUris(List(redirectUrl).asJava) - - oAuth2Client.setRedirectUris(List(redirectUrl).asJava) - // if set client certificate supplied, set it to client meta. - if(consumer.clientCertificate != null && StringUtils.isNotBlank(consumer.clientCertificate.get)) { - val clientMeta = Map("client_certificate" -> consumer.clientCertificate.get).asJava - oAuth2Client.setMetadata(clientMeta) - } - // 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) - - val decoratedClient = fun(oAuth2Client) - logger.debug(s"oAuth2Client: $oAuth2Client") - val oAuth2ClientResult = Some(hydraAdmin.createOAuth2Client(decoratedClient)) - logger.info("createHydraClient process is successful.") - oAuth2ClientResult - } - - - /** - * create jwk - * @param signingAlg signing algorithm name - * @return private key json string to public key - */ - def createJwk(signingAlg: String): (String, String) = { - val keyGenerator = if(signingAlg.startsWith("ES")) { - val curves = Curve.forJWSAlgorithm(JWSAlgorithm.parse(signingAlg)) - val curve:Curve = curves.iterator().next() - new ECKeyGenerator(curve) - } else { - new RSAKeyGenerator(RSAKeyGenerator.MIN_KEY_SIZE_BITS) - } - val jwk: JWK = keyGenerator.keyUse(KeyUse.SIGNATURE) // indicate the intended use of the key - .keyID(UUID.randomUUID().toString()) // give the key a unique ID - .algorithm(new Algorithm(signingAlg)) - .generate() - - jwk.toJSONString -> jwk.toPublicJWK().toJSONString - } -} From b3b4172c0ce3ea77e6400466f1a9b35c25474b78 Mon Sep 17 00:00:00 2001 From: simonredfern Date: Tue, 14 Jul 2026 20:01:32 +0200 Subject: [PATCH 3/3] Fixing for concurrency test --- obp-api/src/main/scala/code/consent/MappedConsent.scala | 5 ++++- .../code/context/MappedUserAuthContextUpdateProvider.scala | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/obp-api/src/main/scala/code/consent/MappedConsent.scala b/obp-api/src/main/scala/code/consent/MappedConsent.scala index d6838479b4..8e0cb38918 100644 --- a/obp-api/src/main/scala/code/consent/MappedConsent.scala +++ b/obp-api/src/main/scala/code/consent/MappedConsent.scala @@ -425,7 +425,10 @@ object MappedConsentProvider extends ConsentProvider with code.util.Helper.MdcLo if (rows == 1) MappedConsent.find(By(MappedConsent.mConsentId, consentId)) else Failure(ErrorMessages.ConsentUpdateStatusError) case _ => - Full(consent) + // Already left INITIATED (e.g. a concurrent answer committed before our read). + // A late second answer must fail like the atomic-transition loser above — + // returning Full here would let two callers pass the SCA gate on one consent. + Failure(ErrorMessages.ConsentUpdateStatusError) } case Empty => Empty ?~! ErrorMessages.ConsentNotFound diff --git a/obp-api/src/main/scala/code/context/MappedUserAuthContextUpdateProvider.scala b/obp-api/src/main/scala/code/context/MappedUserAuthContextUpdateProvider.scala index fbd7811419..92ff60e8e4 100644 --- a/obp-api/src/main/scala/code/context/MappedUserAuthContextUpdateProvider.scala +++ b/obp-api/src/main/scala/code/context/MappedUserAuthContextUpdateProvider.scala @@ -73,7 +73,10 @@ object MappedUserAuthContextUpdateProvider extends UserAuthContextUpdateProvider if (rows == 1) MappedUserAuthContextUpdate.find(By(MappedUserAuthContextUpdate.mUserAuthContextUpdateId, consentId)) else Failure(ErrorMessages.UserAuthContextUpdateStatusError) case _ => - Full(consent) + // Already left INITIATED (e.g. a concurrent answer committed before our read). + // A late second answer must fail like the atomic-transition loser above — + // returning Full here would allow MFA double-authorisation. + Failure(ErrorMessages.UserAuthContextUpdateStatusError) } } }